How do I efficiently replace a storage array in Solidity? - arrays

I have the following function in Solidity which takes as arguments 2 arrays, an array of shareholder addresses and an array of their stakes. I'm keeping an array of shareholders in storage, together with a map to their stakes.
If the updated array is the same size, it's simple, just overwrite each position with the new values. If they are different sizes however, I first go through the entire array and delete each element, and then insert the new ones. I feel this is not very efficient and it could be done better.
PS: I am a complete beginner to Solidity and this is my first contract, so please feel free to let me know if I'm doing anything stupid or inefficiently.
Thanks !
function setShareholders(address[] _shareholders, uint256[] _stakes) public onlyCEO {
require(_shareholders.length > 0);
require(_shareholders.length == _stakes.length);
uint256 accummulator = 0;
for(uint8 x = 0; x < _stakes.length; x++){
require(_addressNotNull(_shareholders[x]));
require(_stakes[x] > 0 && _stakes[x] <= 100);
accummulator = SafeMath.add(accummulator, _stakes[x]);
}
if(accummulator == 100){ // stakes need to add up to 100%
_setShareholders(_shareholders, _stakes);
}
}
function _setShareholders(address[] _shareholders, uint256[] _stakes) private {
if(_shareholders.length == shareholders.length){
for(uint8 x = 0; x < shareholders.length; x++) {
shareholders[x] = _shareholders[x];
shareholderToStake[_shareholders[x]] = _stakes[x];
}
}
else {
for(x = 0; x < shareholders.length; x++) {
delete shareholders[x];
shareholders.length--;
delete shareholderToStake[shareholders[x]];
}
for(x = 0; x < _shareholders.length; x++) {
shareholders.push(_shareholders[x]);
shareholderToStake[_shareholders[x]] = _stakes[x];
}
}
}

In theory, this would work...unfortunately in solidity, managing arrays is a costly nightmare. Doing any array manipulation, on not just one but 2 arrays, is not recommended at all.
You could keep your array of shareholders...but from there, I'd recommend creating a mapping of address->structure. This way you can loop through your mapped structure and contain all necessary data within that.
So something like this for your refactor:
address[] public shareholders;
struct ShareHolder {
uint stake;
// ...other awesome data here
}
mapping (address => ShareHolder) public shareholderData;
This way, you have your shareholders list. And you can directly access a shareholder with shareholderData[<SHAREHOLDER ADDRESS>].

Related

Compare two arrays in memory in Solidity and a return a "not in" result

I have a large contract and I am in the process of splitting it out into two. The goal is to have the functions that are common (and will be used by many other contracts) to be separated out for efficiency.
One of these functions compares items in arrays "ownedSymbols" and "targetAssets". It produces a list "sellSymbols" if any item in "ownedSymbols" is not in "targetAssets".
The below code works fine while "sellSymbols" is stored as a string. As this function will become common, I need it to run in memory so the results aren't confused by calls from different contracts.
pragma solidity >0.8.0;
contract compareArrays {
string[] public ownedSymbols = ["A","B","C"];
string[] public targetAssets = ["A","B"];
string[] sellSymbols;
event sellListEvent(string[]);
function sellList(string[] memory _ownedSymbols, string[] memory _targetAssetsList) internal {
sellSymbols = _ownedSymbols;
for (uint256 i = 0; i < _targetAssetsList.length; i++) {
for (uint256 x = 0; x < sellSymbols.length; x++) {
if (
keccak256(abi.encodePacked((sellSymbols[x]))) ==
keccak256(abi.encodePacked((_targetAssetsList[i])))
) {
if (x < sellSymbols.length) {
sellSymbols[x] = sellSymbols[sellSymbols.length - 1];
sellSymbols.pop();
} else {
delete sellSymbols;
}
}
}
}
emit sellListEvent(sellSymbols);
}
function runSellList() public {
sellList(ownedSymbols,targetAssets);
}
}
Ideally the function would run with "string[] memory sellSymbols", however this kicks back an error.
pragma solidity >0.8.0;
contract compareArrays {
string[] public ownedSymbols = ["A","B","C"];
string[] public targetAssets = ["A","B"];
event sellListEvent(string[]);
function sellList(string[] memory _ownedSymbols, string[] memory _targetAssetsList) internal {
string[] memory sellSymbols = _ownedSymbols;
for (uint256 i = 0; i < _targetAssetsList.length; i++) {
for (uint256 x = 0; x < sellSymbols.length; x++) {
if (
keccak256(abi.encodePacked((sellSymbols[x]))) ==
keccak256(abi.encodePacked((_targetAssetsList[i])))
) {
if (x < sellSymbols.length) {
sellSymbols[x] = sellSymbols[sellSymbols.length - 1];
sellSymbols.pop();
} else {
delete sellSymbols;
}
}
}
}
emit sellListEvent(sellSymbols);
}
function runSellList() public {
sellList(ownedSymbols,targetAssets);
}
}
The error:
TypeError: Member "pop" is not available in string memory[] memory outside of storage.
--> contracts/sellSymbols.sol:20:25:
|
20 | sellSymbols.pop();
| ^^^^^^^^^^^^^^^
Two questions from me:
Is there a way to do this in memory so that the function can be common (i.e. used by multiple contracts at the same time)?
Is there a better way? The below is expensive to run, but it is the only way I have been able to achieve it.
One final comment - I know this would be much easier/cheaper to run off chain. That is not something I am willing to consider as I want this project to be decentralized.
If you want to keep the existing system, the best solution is described here: https://stackoverflow.com/a/49054593/11628256
if (x < sellSymbols.length) {
sellSymbols[x] = sellSymbols[sellSymbols.length - 1];
delete sellSymbols[myArray.length - 1];
sellSymbols.length--;
} else {
delete sellSymbols;
}
If all you care about is the presence or absence of a particular asset (and not enumerating through them), what you're going to want to do to really reduce gas costs is something called "lazy evaluation." Lazy evaluation is when instead of computing all results at once (like increasing all balances by 50% by iterating over an array), you modify the getters so that their return value reflects the operation (such as multiplying an internal variable by 50% and multiplying the original result of getBalance by that variable).
So, if this is the case, what you want to do is use the following function instead:
function except(string _item, mapping(string => bool) _ownedSymbols, mapping(string => bool) _targetAssets) internal returns (bool) {
return _ownedSymbols[_item] && !_targetAssets[_item];
}
<pet peeve>
Finally, I know you say you want this to be decentralized, but I really do feel the urge to say this. If this is a system that doesn't need to be decentralized, don't decentralize it! Decentralization is great for projects that other people rely on - for example, DNS or any sort of token.
From your variable names, it seems that this is probably some sort of system similar to a trading bot. Therefore, the incentive is on you to keep it running, as you are the one that gets the benefits. None of the problems that decentralization solves (censorship, conflict of interest, etc...) apply to your program, as the person running it is incentivized to keep it running and keep a copy of the program. It's cheaper for the user running it to not have security they don't need. You don't need a bank-grade vault to store a $1 bill!
</pet peeve>

How to find duplicate entry in Solidity array

I'm writing a function that is supposed to receive an array of uint's and I want to make a requirement that none of the elements in the array be the same and that all of the elements in the array be part of the pre-selected elements to belong in an array. So far I have:
function vote(uint[] memory proposals) external
{
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote.");
require(!sender.voted, "Already voted.");
mapping(uint => bool) duplicateVotes;
require(!duplicateVotes, "Cannot vote for a proposal more than once.");
sender.voted = true;
sender.vote = proposals;
proposals[proposals].voteCount += sender.weight;
}
But I get an error:
The data location must be "storage", "memory" or "calldata" for variable, but none was given.
But I think this error is just a symptom of a larger problem. Can anyone help me figure out a solution to this problem?
I think u want to achieve that same person does not vote twice but the way you are doing is computationally expensive and this will cost too much gas. Instead, crate a mapping to keep track if voter has voted before:
mapping(address=>bool) peopleWhoVoted;
also you want to keep array of voters
address[] public listOfVoters;
So in your vote function, make sure that caller of this function-msg.sender is not in the peopleWhoVoted mapping
function vote(pass argument) public {
// msg.sender is globally available, it is function caller
require(!peopleWhoVoted[msg.sender],"this caller already voted")
// now u are sure that this caller did not vote before
// so u can add the function caller to the array
listOfVoters.push(msg.sender)
}
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract findDupli {
uint[] public arr = [2,3,4,8,1,7,8];
function funFind() public view returns(uint) {
uint temp;
for(uint i = 0; i < arr.length; i++) {
temp = arr[i];
for(uint j = 0; j < arr.length; j++) {
if((j != i) && (temp == arr[j])) {
return temp;
}
}
}
}
}

JavaFX - Getting the index when clicking something inside a 2d Array

I've built a 2d array of ImageViews, and I want to be able to print out the indices of the ImageView when I click it (and use that information to change the pictures).
I've got the following code right now, but I don't know how to get it to print the x and y values of its location in the 2d array.
ImageView[][] mainGrid = new ImageView[8][8];
...
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
mainGrid[x][y] = new ImageView(image);
mainGrid[x][y].setOnMouseClicked(createMouseHandler());
}
}
...
private EventHandler<? super MouseEvent> createMouseHandler() {
return event -> {
System.out.println("Clicked");
}
}
Any help is greatly appreciated! Thank you!
The most general solution to this problem is to use the getProperties() method of the Node class. You can use that to store any user information in a node you like and then later retrieve that if you need it. In your case store the indices and then retrieve them in your mouse handler.

Select random elements from an array without repeats?

edit: I can't believe I didn't catch this sooner. Turns out my problem was re-declaring my first variables over and over again, essentially starting the program fresh instead of continuing it. To fix it, I replaced the first two lines with this:
if (initialized === undefined) {
trace("INITIALIZING");
var MCs = [];
var lastPos = "intializer";
var initialized = 1;
}
Now it works like a charm. I feel like a noob for this one; sorry to anyone whose time I wasted. I'd post this as an answer to my own question, but it won't let me since I'm still new.
Original Post follows:
I'm trying to make a flash that will randomly choose an ad, play it, and then randomly play another. To that end, I've succeeded by shuffling an array, and then gotoAndPlay-ing the label in the first element of the array, and then removing that element. At the end of each ad is gotoAndPlay(1); with all the main code being on the first frame. If the array is empty, it rebuilds it and reshuffles it.
The problem is, I don't want it to repeat any ads until its run through all of them; I think I've got that down, but I'm not positive. Further, I don't want the last element in the array to be the same as the first in the new one, so the same ad won't ever show twice in a row. I'm trying to have it detect if the element it just used matches the one it's about to use, and reshuffle if that happens, but in my testing it continues to occasionally show the same ad twice in a row.
I'm obviously doing something wrong, but being entirely new to ActionScript3 (and in fact to flash) I'm having a lot of trouble identifying what it is. Here's what I have right now:
var MCs = [];
var lastPos = "intializer";
if (MCs.length == 0) {
MCs = reset();
if (lastPos == MCs[0]) {
while (lastPos == MCs[0]) {
MCs = reset();
}
}
}
if (MCs.length > 0) {
lastPos = MCs[0];
MCs.splice(0,1);
gotoAndPlay(lastPos+"MC");
}
function reset(){
var PrepMCs = new Array("Image1", "Image2", "Image3");
var WorkMCs = new Array(PrepMCs.length);
var randomPos:Number = 0;
for (var i:int = 0; i < WorkMCs.length; i++)
{
randomPos = int(Math.random() * PrepMCs.length);
WorkMCs[i] = PrepMCs.splice(randomPos, 1)[0];
}
return WorkMCs;
}
Personally, I'd rather just do this with JavaScript, HTML, and images; it'd be really simple. But for hosting/CMS reasons I don't have any control over, I'm limited to a single file or a single block of code; I can't host anything externally, which as far as I can tell leaves Flash as my best option for this.
Any help would be greatly appreciated, thanks! If I've done something horribly, horribly wrong, and it's a wonder this even runs at all, don't hesitate to tell me!
edit: It just occurred to me, it is perfectly fine if the second run is in the same order as the first run, etc. The main thing is, it needs to be random. This is probably much easier to implement.
edit 2: MASSIVE DERP HERE. Every time it runs, it re-initializes MCs and lastPos... in other words, it's shuffling every time and starting over. What I should be researching is how to only run a line of code if a variable hasn't been initialized yet.
Blatantly stealing from #32bitKid, this is my version.
The main problem I have with his solution is the push/splice idea. As much as possible, I like to create once, and reuse. Shrinking and growing arrays is bulky, even if effective.
Also, this method does not re-order the array, which may or may not be valuable.
BTW, I like the way that he prevents a repeat of the previous item ("almost empty").
So here is another method:
package
{
public class RandomizedList
{
private var _items:Array;
private var idxs:Array;
private var rnd:int;
private var priorItemIdx:int;
private var curIdx:int;
public function RandomizedList(inarr:Array)
{
items = inarr;
}
private function initRandomize():void
{
idxs = new Array();
//Fisher-Yates initialization (http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle):
idxs[i] = 0;
for (var i:int = 1; i < items.length; i++)
{
rnd = int(Math.random() * (i + 1));
idxs[i] = idxs[rnd];
idxs[rnd] = rnd;
}
curIdx = 0;
priorItemIdx = -1;
}
private function randomize():void
{
var tempint:int;
//Fisher-Yates (http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle):
for (var i:int = items.length; i >= 1; i--)
{
rnd = int(Math.random() * (i + 1));
tempint = idxs[i];
idxs[i] = idxs[rnd];
idxs[rnd] = tempint;
}
curIdx = 0;
}
public function next():void
{
if (curIdx >= idxs.length)
{
randomize();
}
if (items.length > 1 && priorItemIdx == idxs[curIdx])
{
curIdx++;
}
priorItemIdx = idxs[curIdx++];
return items[priorItemIdx];
}
public function get items():Array
{
return _items;
}
public function set items(value:Array):void
{
_items = value;
initRandomize();
}
}
}
I would use a utility class like this to abstract out the behavior I wanted:
import flash.text.TextField;
class Randomizer {
private var unused:Array = [];
private var used:Array;
public function Randomizer(playList:Array) {
used = playList;
}
public function next():* {
// If almost empty, refill the unused array
if(unused.length <= 1) refill();
// Get the first item off the playList
var item:* = unused.shift();
// Shove it into the bucket
used.push(item);
// return it back
return item;
}
public function refill():void {
var i:int;
// Fisher-Yates shuffle to refill the unused array
while(used.length > 0) {
i = Math.floor(Math.random() * used.length)
unused.push(used.splice(i,1)[0])
}
}
}
Notice that it refills the unused array when the unused array still has one item in it, this makes it impossible for the last result to repeat twice in a row. This will return each item once before before looping, and will never repeat the same item twice.
You would use it by saying something like:
var ads:Randomizer = new Randomizer(["Image1", "Image2", "Image3"]);
ads.next(); // will return something
ads.next(); // will return something
ads.next(); // will return something
ads.next(); // will return something
// Keep going into infinity...
There is a little test example of this code working here.
See if this makes any sense
//create your array of all your ad names/frame labels
var PrepMCs:Array = new Array("Image1", "Image2", "Image3");
var shuffledMCs:Array = [];
//store the name of the last played ad in this var
var lastAdPlayed:String;
//shuffle the array
shuffleArray(PrepMCs);
function shuffleArray(arrayToShuffle:Array):void {
//clear the array
shuffledMCs = [];
var len:int = arrayToShuffle.length;
for(var i:int = 0; i<len; i++) {
shuffledMCs[i] = arrayToShuffle.splice(int(Math.random() * (len - i)), 1)[0];
}
//test to see if the new first ad is the same as the last played ad
if (lastAdPlayed == shuffledMCs[0]) {
//reshuffle
shuffleArray(PrepMCs);
} else {
lastAdPlayed = [0];
trace(shuffledMCs);
playAds();
}
}
//after each ad has played, call this function
function playAds():void {
if (shuffledMCs.length > 0) {
gotoAndPlay(shuffledMCs[0]);
shuffledMCs.splice(0,1);
} else {
//array is empty so we have played all the ads
shuffleArray(PrepMCs);
}
}

Moving objects in array

I have an array which is filled with platforms that are supposed to move.
var MovingPlatformArray:Array = new Array();
for (var c:int = numChildren - 1; c >= 0; c--){
var child3:DisplayObject = getChildAt(c);
if (child3.name == "movingplatform"){
MovingPlatformArray.push(child3);
}
}
this.addEventListener(Event.ENTER_FRAME,ctrl_birdie);
function ctrl_birdie(e:Event):void{
for(var c in MovingPlatformArray){
MovingPlatform[c].y += speed;
if(MovingPlatformArray[c].hitTestPoint(birdie.x,birdie.y,true)){
birdtelleryvertrager=0;
birdtellery = 0;
birdie.y-=14;
}
if(movingplatform.y <= 25){
speed = 2;
}
if(movingplatform.y >= 350){
speed = -2;
}
}
Right now I have 2 moving platforms in this array. But only one moves up and down. But they both register a touch with the birdie. Am I doing something wrong?
In your listener, you're only setting the position of one platform, which ever one "movingplatform" is a reference to. As all your stage instances of moving platforms are named "movingplatform", one lucky platform is getting referenced by name (the rest ignored), instead of what you intended, which is to use the references in your array and adjust each platform.
You probably meant for movingplatform to be a local variable in your event handler, declared something like this:
var movingplatform:DisplayObject = MovingPlatformArray[c] as DisplayObject;
I'd recommend using a for each loop in place of the for in, because I think it's a little cleaner, but this is a minor style thing:
for each (var platform:DisplayObject in MovingPlatformArray)
{
platform.y += speed;
... rest of your code ...
}
For the sake of clarity, I edited the loop variable to be platform instead of movingplatform, to avoid confusion of having a local variable shadow a stage instance (i.e. this.movingplatform). I wanted it to be clear that the stage instance name is not being used here, because the unintentional instance name reference in your code is the source of your problem in the first place.
As far as i'm concerned, you have two options. use a for each, as adam smith suggested or use a for-loop as it was intended to be used :)
for(var c:uint = 0; c < MovingPlatformArray.length; c++){...
and btw: should "MovingPlatform[c].y += speed;" not be "MovingPlatformArray[c].y += speed;"?
edit: looking at your code, i would also suggest you use MovingPlatformArray[c].hitTestObject(birdie) instead of MovingPlatformArray[c].hitTestPoint(birdie.x,birdie.y,true)
If I were you, I would bring the logic for the platform out, and store it in a class. (Ideally you would do this for the birdie object as well). I have created an example below. The movieclips on the stage should extend Platform rather than MovieClip so they invoke the methods at the bottom.
// Use vectors if you know all the items are going to be the same type
var platforms:Vector.<Platform> = new <Platform>[];
for (var c:int = numChildren - 1; c >= 0; c--){
var child:DisplayObject = getChildAt(c);
// You shouldn't check against names (as per the original post). Because
// names should be unique
if (child is Platform){
platforms.push(child);
// This could be random so each platform has a different range
// This means platform 1 could go from y 30 to y 400, platform 2
// could go from y 60 to y 200, etc
child.setRange(25, 400);
}
}
this.addEventListener(Event.ENTER_FRAME, gameLoop);
// Have an overall game loop
function gameLoop(e:Event):void {
// Loop over the platforms
platforms.forEach(function(item:Platform, i:int, a:Vector.<Platform>):void {
// Hit test function in the class means you only have to pass in one mc
// rather than the points and a boolean
if(item.hitTest(birdie)) {
birdtelleryvertrager=0;
birdtellery = 0;
birdie.y-=14;
}
// Removed the movement logic, this should be kept out of the game loop
// plus how much better does this read?
item.move();
});
}
Then in a class location somewhere, like in a folder game/activeObjects
// A class for the platform stored else where
package game.activeObjects
{
import flash.display.MovieClip;
/**
*
*/
public class Platform extends MovieClip {
private const SPEED:Number = 2;
private var _direction:int = 1;
private var _minimumHeight:Number = 25;
private var _maximumHeight:Number = 350;
public function Platform() {
}
public function setRange(minimumHeight:Number, maximumHeight:Number) {
_minimumHeight = minimumHeight;
_maximumHeight = maximumHeight;
}
public function move():void {
this.y += SPEED * _direction;
if(this.y <= _minimumHeight) {
_direction = 1;
} else if(this.y >= _maximumHeight) {
_direction = -1;
}
}
public function hitTest(mc:MovieClip):Boolean {
return hitTestPoint(mc.x,mc.y,true);
}
}
}

Resources