Solidity contract for storing and interacting with key/value string pairs
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
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 ofyour_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 thenode_modules
subs-directories
Review the Truffle -- Package Management via NPM documentation for more details.
How to utilize this repository
Write a set of contracts that make use of, and extend, StringStorage
features.
// 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 withindata
contract, such as preferred user-name -
restricts certain mutation actions to owner only
-
tracks references to
Host
and relatedStringStorage
instances
Note, in this example the
Host
toStringStorage
references are configured such thatAccount().owner
may write host specific information, such as avatar URL. However, be aware it is up to eachHost
site if data withinAccount().data
and/orAccount().registered.get(_ref_)
are used, and how.
// 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 individualAccount
owners -
tracks references to
Account
and relatedStringStorage
instances
Note, in this example the
Account
toStringStorage
references are configured such thatHost().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.
Application Programming Interfaces for Solidity smart contracts
Solidity contract for storing and interacting with key/value string pairs
Source contracts/StringStorage.sol
Properties
-
data
{mapping(string => string)} Store key/valuestring
pairs -
indexes
{mapping(string => uint256)} Warning order of indexes NOT guaranteed! -
keys
{string[]} Warning order of keys NOT guaranteed! -
owner
{address} Allow mutation from specifiedaddress
-
authorized
{mapping(address => bool)} Allow mutation from specifiedaddress
s
Developer note -> Depends on
@solidity-utilities/library-mapping-string
Insert
address
intomapping
ofauthorized
data structure
Source addAuthorized(address _key)
Parameters
_key
{address} Key to set value oftrue
Throws -> {Error} "StringStorage.addAuthorized: message sender not an owner"
Developer note -> Does not check if address
is already authorized
Overwrite old
owner
with new owneraddress
Source changeOwner(address _new_owner)
Parameters
_new_owner
{address} New owner address
Throws -> {Error} "StringStorage.changeOwner: message sender not an owner"
Delete
mapping
string key/value pairs and remove allstring
fromkeys
Source clear()
Throws -> {Error} "StringStorage.clar: message sender not an owner"
Developer note -> Warning may fail if storing many string
pairs
Remove
address
frommapping
ofauthorized
data structure
Source deleteAuthorized(address _key)
Parameters
_key
{address} Key to set value offalse
Throws
-
{Error}
"StringStorage.deleteAuthorized: message sender not authorized"
-
{Error}
"StringStorage.deleteAuthorized: cannot remove owner"
Retrieve stored value
string
or throws an error if undefined
Source get(string _key)
Parameters
_key
{string} Mapping keystring
to lookup corresponding valuestring
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
Retrieve stored value
string
or provided defaultstring
if undefined
Source getOrElse(string _key, string _default)
Parameters
-
_key
{string} Mapping keystring
to lookup corresponding valuestring
for -
_default
{string} Value to return if keystring
lookup is undefined
Returns -> {string} Value string
for given key string
or _default
if undefined
Developer note -> Forwards parameters to
data.getOrElse
Allow for defining custom error reason if value
string
is undefined
Source getOrError(string _key, string _reason)
Parameters
-
_key
{string} Mapping keystring
to lookup corresponding valuestring
for -
_reason
{string} Custom error message to throw if valuestring
is undefined
Returns -> {string} Value for given key string
Throws -> {Error} _reason
if value is undefined
Developer note -> Forwards parameters to
data.getOrError
Check if
string
key has a corresponding valuestring
defined
Source has(string _key)
Parameters
_key
{string} Mapping key to check if valuestring
is defined
Returns -> {bool} true
if value string
is defined, or false
if
undefined
Developer note -> Forwards parameter to
data.has
Index for
string
key withinkeys
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
Index for
string
key withinkeys
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
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
Delete value
string
for given_key
Source remove(string _key)
Parameters
_key
{string} Mapping key to delete corresponding valuestring
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
Delete value
string
for given_key
Source removeOrError(string _key, string _reason)
Parameters
-
_key
{string} Mapping key to delete corresponding valuestring
for -
_reason
{string} Custom error message to throw if valuestring
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
Call
selfdestruct
with providedaddress
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"
Store
_value
under given_key
while preventing unintentional overwrites
Source set(string _key, string _value)
Parameters
-
_key
{string} Mapping key to set corresponding valuestring
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
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 valuestring
for -
_value
{string} Mapping value to set -
_reason
{string} Custom error message to present if valuestring
is defined
Throws
-
{Error}
"StringStorage.setOrError: message sender not an owner"
-
{Error}
_reason
if value is defined
Developer note -> Forwards parameters to
data.setOrError
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
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.
Options for contributing to string-storage and solidity-utilities
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 ofinstall
to avoid mutating thepackage.json
, and/orpackage-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.
Methods for financially supporting
solidity-utilities
that maintainsstring-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.
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.