Skip to content

Solidity contract for storing and interacting with key/value string pairs

License

Notifications You must be signed in to change notification settings

solidity-utilities/string-storage

Repository files navigation

String Storage

Solidity contract for storing and interacting with key/value string pairs

Byte size of String Storage Open Issues Open Pull Requests Latest commits Build Status



Requirements

Prerequisites and/or dependencies that this project needs to function properly

This project utilizes Truffle for organization of source code and tests, thus it is recommended to install Truffle globally to your current user account

npm install -g truffle

Quick Start

Perhaps as easy as one, 2.0,...

NPM and Truffle are recommended for importing and managing dependencies

cd your_project

npm install @solidity-utilities/string-storage

Note, source code will be located within the node_modules/@solidity-utilities/string-storage directory of your_project root

Solidity contracts may then import code via similar syntax as shown

import {
    StringStorage
} from "@solidity-utilities/string-storage/contracts/StringStorage.sol";

Note, above path is not relative (ie. there's no ./ preceding the file path) which causes Truffle to search the node_modules subs-directories

Review the Truffle -- Package Management via NPM documentation for more details.


Usage

How to utilize this repository

Write a set of contracts that make use of, and extend, StringStorage features.

contracts/Account.sol

// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.7;

import {
  AddressStorage
} from "@solidity-utilities/address-storage/contracts/AddressStorage.sol";

import {
    StringStorage
} from "@solidity-utilities/string-storage/contracts/StringStorage.sol";

import { Host } from "./Host.sol";

/// @title Example contract instance to track `Host` references
/// @author S0AndS0
contract Account {
    address payable public owner;
    /// Interface for `mapping(string => string)`
    StringStorage public data;
    /// Interfaces mapping references of Host to StringStorage(this)
    AddressStorage public registered;
    AddressStorage public removed;

    /* -------------------------------------------------------------------- */

    ///
    constructor(address payable _owner) {
        owner = _owner;
        data = ownedStringStorage();
        registered = ownedAddressStorage();
        removed = ownedAddressStorage();
    }

    /* -------------------------------------------------------------------- */

    /// @notice Require message sender to be an instance owner
    /// @param _caller {string} Function name that implements this modifier
    modifier onlyOwner(string memory _caller) {
        string memory _message = string(
            abi.encodePacked(
                "Account.",
                _caller,
                ": message sender not an owner"
            )
        );
        require(msg.sender == owner, _message);
        _;
    }

    /* -------------------------------------------------------------------- */

    /// @notice Record registration of hosts
    event HostRegistered(address host_reference, address storage_reference);

    /// @notice Record removal of hosts
    event HostRemoved(address host_reference, address storage_reference);

    /* -------------------------------------------------------------------- */

    /// @notice Update `Account.owner`
    /// @dev Changing ownership for `data` and `registered` elements may be a good idea
    /// @param _new_owner **{address}** Address to assign to `Account.owner`
    /// @custom:throws **{Error}** `"Account.changeOwner: message sender not an owner"`
    function changeOwner(address payable _new_owner)
        external
        onlyOwner("changeOwner")
    {
        owner = _new_owner;
    }

    /// @notice Add `Host` reference to `registered` data structure
    /// @dev Likely a better idea to pre-check if has `registered` and `removed`
    /// @param _host_reference **{address}** Reference to `Host` to registered to
    /// @custom:throws **{Error}** `"Account.hostRegister: message sender not an owner"`
    /// @custom:throws **{Error}** `"Account.hostRegister: host already registered"`
    function hostRegister(address _host_reference)
        external
        payable
        onlyOwner("hostRegister")
    {
        require(
            !removed.has(_host_reference),
            "Account.hostRegister: host is removed"
        );
        Host(_host_reference).accountRegister{ value: msg.value }(
            address(this)
        );
        StringStorage _storage_instance = ownedStringStorage();
        registered.setOrError(
            _host_reference,
            address(_storage_instance),
            "Account.hostRegister: host already registered"
        );
        emit HostRegistered(_host_reference, address(_storage_instance));
    }

    /// @notice Move `Host` from `registered` to `removed` data structure
    /// @param _host_reference **{address}** Reference to `Host` to move
    /// @return **{address}** Reference to `StringStorage` for given `Host`
    /// @custom:throws **{Error}** `"Account.hostRemove: message sender not an owner"`
    /// @custom:throws **{Error}** `"Account.hostRemove: host not registered"`
    /// @custom:throws **{Error}** `"Account.hostRemove: host was removed"`
    function hostRemove(address _host_reference)
        external
        onlyOwner("hostRemove")
        returns (address)
    {
        removed.setOrError(
            _host_reference,
            registered.removeOrError(
                _host_reference,
                "Account.hostRemove: host not registered"
            ),
            "Account.hostRemove: host was removed"
        );
        address _storage_reference = removed.get(_host_reference);
        emit HostRemoved(_host_reference, _storage_reference);
        Host _host_instance = Host(_host_reference);
        if (_host_instance.registered().has(address(this))) {
            _host_instance.accountRemove(address(this));
        }
        return _storage_reference;
    }

    ///
    function ownedStringStorage() internal returns (StringStorage) {
        StringStorage _instance = new StringStorage(address(this));
        _instance.addAuthorized(address(this));
        _instance.changeOwner(owner);
        return _instance;
    }

    ///
    function ownedAddressStorage() internal returns (AddressStorage) {
        AddressStorage _instance = new AddressStorage(address(this));
        _instance.addAuthorized(address(this));
        _instance.changeOwner(owner);
        return _instance;
    }
}

The above Account.sol contract;

  • allows for storing owner information within data contract, such as preferred user-name

  • restricts certain mutation actions to owner only

  • tracks references to Host and related StringStorage instances

Note, in this example the Host to StringStorage references are configured such that Account().owner may write host specific information, such as avatar URL. However, be aware it is up to each Host site if data within Account().data and/or Account().registered.get(_ref_) are used, and how.

contracts/Host.sol

// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.7;

import {
  AddressStorage
} from "@solidity-utilities/address-storage/contracts/AddressStorage.sol";

import {
    StringStorage
} from "@solidity-utilities/string-storage/contracts/StringStorage.sol";

import { Account } from "./Account.sol";

/// @title Example contract instance to track `Account` references
/// @author S0AndS0
contract Host {
    address payable public owner;
    /// Mitigate abuse by setting a signup fee to refund `accountBan` gas costs
    uint256 public fee;
    /// Interface for `mapping(string => string)` of host information
    StringStorage public data;
    /// Interfaces mapping references of Account to StringStorage(this)
    AddressStorage public registered;
    AddressStorage public removed;

    /* -------------------------------------------------------------------- */

    ///
    constructor(address payable _owner, uint256 _fee) {
        owner = _owner;
        fee = _fee;
        data = ownedStringStorage();
        registered = ownedAddressStorage();
        removed = ownedAddressStorage();
    }

    /* -------------------------------------------------------------------- */

    /// @notice Record registration of accounts
    event AccountRegistered(
        address account_reference,
        address storage_reference
    );

    /// @notice Record removal of accounts
    event AccountRemoved(address account_reference, address storage_reference);

    /* -------------------------------------------------------------------- */

    /// @notice Add initialized `Account` to `registered` data structure
    /// @param _account_reference **{address}** Previously deployed `Account` contract instance
    /// @custom:throws **{Error}** `"Host.accountRegister: insufficient funds provided"`
    /// @custom:throws **{Error}** `"Host.accountRegister: account already registered or removed"`
    /// @custom:throws **{Error}** `"Host.accountRegister: message sender not authorized"`
    function accountRegister(address _account_reference) external payable {
        require(
            msg.value >= fee,
            "Host.accountRegister: insufficient funds provided"
        );
        require(
            !registered.has(_account_reference) &&
                !removed.has(_account_reference),
            "Host.accountRegister: account already registered or removed"
        );
        require(
            msg.sender == _account_reference ||
                msg.sender == Account(_account_reference).owner(),
            "Host.accountRegister: message sender not authorized"
        );
        address _storage_reference = address(ownedStringStorage());
        registered.set(_account_reference, _storage_reference);
        emit AccountRegistered(_account_reference, _storage_reference);
    }

    /// @notice Move `Account` from `registered` to `removed` data structure
    /// @param _account_reference **{address}** Reference of `Account` to move
    /// @return **{address}** Reference to `StringStorage` for given `Account`
    /// @custom:throws **{Error}** `"Host.accountRemove: message sender not authorized"`
    /// @custom:throws **{Error}** `"Host.accountRemove: account not registered"`
    /// @custom:throws **{Error}** `"Host.accountRemove: account was removed"`
    function accountRemove(address _account_reference)
        external
        returns (address)
    {
        require(
            msg.sender == _account_reference ||
                msg.sender == Account(_account_reference).owner() ||
                msg.sender == owner,
            "Host.accountRemove: message sender not authorized"
        );
        removed.setOrError(
            _account_reference,
            registered.removeOrError(
                _account_reference,
                "Host.accountRemove: account not registered"
            ),
            "Host.accountRemove: account was removed"
        );
        owner.transfer(fee);
        address _storage_reference = removed.get(_account_reference);
        emit AccountRemoved(_account_reference, _storage_reference);
        return _storage_reference;
    }

    /// @notice Update fee to execute `Host.accountRegister`
    /// @custom:throws **{Error}** `"Host.setFee: message sender not owner"`
    function setFee(uint256 _fee) external {
        require(
            msg.sender == owner,
            "Host.setFee: message sender not an owner"
        );
        fee = _fee;
    }

    ///
    function ownedStringStorage() internal returns (StringStorage) {
        StringStorage _instance = new StringStorage(address(this));
        _instance.addAuthorized(address(this));
        _instance.changeOwner(owner);
        return _instance;
    }

    ///
    function ownedAddressStorage() internal returns (AddressStorage) {
        AddressStorage _instance = new AddressStorage(address(this));
        _instance.addAuthorized(address(this));
        _instance.changeOwner(owner);
        return _instance;
    }
}

The above Host.sol contract;

  • allows for storing information within data contract, such as site URL

  • restricts certain mutation actions to owner or individual Account owners

  • tracks references to Account and related StringStorage instances

Note, in this example the Account to StringStorage references are configured such that Host().owner may write host specific information, such as site permissions.


There is much more that can be accomplished by leveraging abstractions provided by StringStorage, check the API section for full set of features available. And review the test/test__examples__Account.js and test/test__examples__Host.js files for inspiration on how to use these examples within projects.


API

Application Programming Interfaces for Solidity smart contracts


Contract StringStorage

Solidity contract for storing and interacting with key/value string pairs

Source contracts/StringStorage.sol

Properties

  • data {mapping(string => string)} Store key/value string pairs

  • indexes {mapping(string => uint256)} Warning order of indexes NOT guaranteed!

  • keys {string[]} Warning order of keys NOT guaranteed!

  • owner {address} Allow mutation from specified address

  • authorized {mapping(address => bool)} Allow mutation from specified addresss

Developer note -> Depends on @solidity-utilities/library-mapping-string


Method addAuthorized

Insert address into mapping of authorized data structure

Source addAuthorized(address _key)

Parameters

  • _key {address} Key to set value of true

Throws -> {Error} "StringStorage.addAuthorized: message sender not an owner"

Developer note -> Does not check if address is already authorized


Method changeOwner

Overwrite old owner with new owner address

Source changeOwner(address _new_owner)

Parameters

  • _new_owner {address} New owner address

Throws -> {Error} "StringStorage.changeOwner: message sender not an owner"


Method clear

Delete mapping string key/value pairs and remove all string from keys

Source clear()

Throws -> {Error} "StringStorage.clar: message sender not an owner"

Developer note -> Warning may fail if storing many string pairs


Method deleteAuthorized

Remove address from mapping of authorized data structure

Source deleteAuthorized(address _key)

Parameters

  • _key {address} Key to set value of false

Throws

  • {Error} "StringStorage.deleteAuthorized: message sender not authorized"

  • {Error} "StringStorage.deleteAuthorized: cannot remove owner"


Method get

Retrieve stored value string or throws an error if undefined

Source get(string _key)

Parameters

  • _key {string} Mapping key string to lookup corresponding value string for

Returns -> {string} Value for given key string

Throws -> {Error} "StringStorage.get: value not defined"

Developer note -> Passes parameter to data.getOrError with default Error _reason to throw


Method getOrElse

Retrieve stored value string or provided default string if undefined

Source getOrElse(string _key, string _default)

Parameters

  • _key {string} Mapping key string to lookup corresponding value string for

  • _default {string} Value to return if key string lookup is undefined

Returns -> {string} Value string for given key string or _default if undefined

Developer note -> Forwards parameters to data.getOrElse


Method getOrError

Allow for defining custom error reason if value string is undefined

Source getOrError(string _key, string _reason)

Parameters

  • _key {string} Mapping key string to lookup corresponding value string for

  • _reason {string} Custom error message to throw if value string is undefined

Returns -> {string} Value for given key string

Throws -> {Error} _reason if value is undefined

Developer note -> Forwards parameters to data.getOrError


Method has

Check if string key has a corresponding value string defined

Source has(string _key)

Parameters

  • _key {string} Mapping key to check if value string is defined

Returns -> {bool} true if value string is defined, or false if undefined

Developer note -> Forwards parameter to data.has


Method indexOf

Index for string key within keys array

Source indexOf(string _key)

Parameters

  • _key {string} Key to lookup index for

Returns -> {uint256} Current index for given _key within keys array

Throws -> {Error} "StringStorage.indexOf: key not defined"

Developer note -> Passes parameter to indexOfOrError with default _reason


Method indexOfOrError

Index for string key within keys array

Source indexOfOrError(string _key, string _reason)

Parameters

  • _key {string} Key to lookup index for

Returns -> {uint256} Current index for given _key within keys array

Throws -> {Error} _reason if value for _key is undefined

Developer note -> Cannot depend on results being valid if mutation is allowed between calls


Method listKeys

Convenience function to read all mapping key strings

Source listKeys()

Returns -> {string[]} Keys string array

Developer note -> Cannot depend on results being valid if mutation is allowed between calls


Method remove

Delete value string for given _key

Source remove(string _key)

Parameters

  • _key {string} Mapping key to delete corresponding value string for

Returns -> {string} Value string that was removed from data storage

Throws

  • {Error} "StringStorage.remove: message sender not an owner"

  • {Error} "StringStorage.remove: value not defined"

Developer note -> Passes parameter to removeOrError with default _reason


Method removeOrError

Delete value string for given _key

Source removeOrError(string _key, string _reason)

Parameters

  • _key {string} Mapping key to delete corresponding value string for

  • _reason {string} Custom error message to throw if value string is undefined

Returns -> {string} Value string that was removed from data storage

Throws

  • {Error} "StringStorage.removeOrError: message sender not an owner"

  • {Error} _reason if value is undefined

Developer note -> Warning reorders keys, and mutates indexes, for efficiency reasons


Method selfDestruct

Call selfdestruct with provided address

Source selfDestruct(address payable _to)

Parameters

  • _to {address} Where to transfer any funds this contract has

Throws -> {Error} "StringStorage.selfDestruct: message sender not an owner"


Method set

Store _value under given _key while preventing unintentional overwrites

Source set(string _key, string _value)

Parameters

  • _key {string} Mapping key to set corresponding value string for

  • _value {string} Mapping value to set

Throws

  • {Error} "StringStorage.set: message sender not an owner"

  • {Error} "StringStorage.set: value already defined"

Developer note -> Forwards parameters to setOrError with default _reason


Method setOrError

Store _value under given _key while preventing unintentional overwrites

Source setOrError(string _key, string _value, string _reason)

Parameters

  • _key {string} Mapping key to set corresponding value string for

  • _value {string} Mapping value to set

  • _reason {string} Custom error message to present if value string is defined

Throws

  • {Error} "StringStorage.setOrError: message sender not an owner"

  • {Error} _reason if value is defined

Developer note -> Forwards parameters to data.setOrError


Method size

Number of key/value string pairs currently stored

Source size()

Returns -> {uint256} Length of keys array

Developer note -> Cannot depend on results being valid if mutation is allowed between calls


Notes

Additional things to keep in mind when developing

In some cases it may be cheaper for deployment costs to use the library-mapping-string project directly instead, especially if tracking defined keys is not needed.


Warning information stored by StringStorage instances should be considered public. Or in other words it is a bad idea to store sensitive or private data on the blockchain.


This repository may not be feature complete and/or fully functional, Pull Requests that add features or fix bugs are certainly welcomed.


Contributing

Options for contributing to string-storage and solidity-utilities


Forking

Tips for forking string-storage

Make a Fork of this repository to an account that you have write permissions for.

  • Clone fork URL. The URL syntax is git@github.com:<NAME>/<REPO>.git, then add this repository as a remote...
mkdir -p ~/git/hub/solidity-utilities

cd ~/git/hub/solidity-utilities

git clone --origin fork git@github.com:<NAME>/string-storage.git

git remote add origin git@github.com:solidity-utilities/string-storage.git
  • Install development dependencies
cd ~/git/hub/solidity-utilities/string-storage

npm ci

Note, the ci option above is recommended instead of install to avoid mutating the package.json, and/or package-lock.json, file(s) implicitly

  • Commit your changes and push to your fork, eg. to fix an issue...
cd ~/git/hub/solidity-utilities/string-storage


git commit -F- <<'EOF'
:bug: Fixes #42 Issue


**Edits**


- `<SCRIPT-NAME>` script, fixes some bug reported in issue
EOF


git push fork main
  • Then on GitHub submit a Pull Request through the Web-UI, the URL syntax is https://github.com/<NAME>/<REPO>/pull/new/<BRANCH>

Note; to decrease the chances of your Pull Request needing modifications before being accepted, please check the dot-github repository for detailed contributing guidelines.


Sponsor

Methods for financially supporting solidity-utilities that maintains string-storage

Thanks for even considering it!

Via Liberapay you may [![sponsor__shields_io__liberapay]][sponsor__link__liberapay] on a repeating basis.

For non-repeating contributions Ethereum is accepted via the following public address;

0x5F3567160FF38edD5F32235812503CA179eaCbca

Regardless of if you're able to financially support projects such as string-storage that solidity-utilities maintains, please consider sharing projects that are useful with others, because one of the goals of maintaining Open Source repositories is to provide value to the community.


Attribution


License

Legal side of Open Source

Solidity contract for storing and interacting with key/value string pairs
Copyright (C) 2021 S0AndS0

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

For further details review full length version of AGPL-3.0 License.