docs: added some cookbook recipes
This commit is contained in:
parent
32915634be
commit
77fcc7fdab
66
docs.wrm/cookbook/ens.wrm
Normal file
66
docs.wrm/cookbook/ens.wrm
Normal file
@ -0,0 +1,66 @@
|
||||
_section: Cookbook: ENS Recipes @<cookbook-ens>
|
||||
|
||||
Here is a collection of short, but useful examples of working with
|
||||
ENS entries.
|
||||
|
||||
|
||||
_subsection: Get all Text rectods @<cookbook-ens-allText>
|
||||
|
||||
Here is a short recipe to get all the text records set for an ENS
|
||||
name.
|
||||
|
||||
It first queries all ``TextChanged`` events on the resovler, and
|
||||
uses a MulticallProvider to batch all the ``eth_call`` queries
|
||||
for each key into a single ``eth_call``. As such, you will need
|
||||
to install:
|
||||
|
||||
``/home/ricmoo> npm install @ethers-ext/provider-multicall``
|
||||
|
||||
|
||||
_code: Fetching all ENS text records. @lang<script>
|
||||
|
||||
mport { ethers } from "ethers";
|
||||
import { MulticallProvider } from "@ethers-ext/provider-multicall";
|
||||
|
||||
async function getTextRecords(_provider, name) {
|
||||
// Prepare a multicall-based provider to batch all the call operations
|
||||
const provider = new MulticallProvider(_provider);
|
||||
|
||||
// Get the resolver for the given name
|
||||
const resolver = await provider.getResolver(name);
|
||||
|
||||
// A contract instance; used filter and parse logs
|
||||
const contract = new ethers.Contract(resolver.address, [
|
||||
"event TextChanged(bytes32 indexed node, string indexed _key, string key)"
|
||||
], provider);
|
||||
|
||||
// A filter for the given name
|
||||
const filter = contract.filters.TextChanged(ethers.namehash(name));
|
||||
|
||||
// Get the matching logs
|
||||
const logs = await contract.queryFilter(filter);
|
||||
|
||||
// Filter the *unique* keys
|
||||
const keys = [ ...(new Set(logs.map((log) => log.args.key))) ];
|
||||
|
||||
// Get the values for the keys; failures are discard
|
||||
const values = await Promise.all(keys.map((key) => {
|
||||
try {
|
||||
return resolver.getText(key);
|
||||
} catch (error) { }
|
||||
return null;
|
||||
}));
|
||||
|
||||
// Return a Map of the key/value pairs
|
||||
return keys.reduce((accum, key, index) => {
|
||||
const value = values[index];
|
||||
if (value != null) { accum.set(key, value); }
|
||||
return accum;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
// Example usage
|
||||
(async function() {
|
||||
const provider = new ethers.InfuraProvider();
|
||||
console.log(await getTextRecords(provider, "ricmoo.eth"));
|
||||
})();
|
7
docs.wrm/cookbook/index.wrm
Normal file
7
docs.wrm/cookbook/index.wrm
Normal file
@ -0,0 +1,7 @@
|
||||
_section: Cookbook @<cookbook>
|
||||
|
||||
A growing collection of code snippets for common problems and use cases
|
||||
when developing dapps and other blockchain tools.
|
||||
|
||||
- [Signing Messages and Data](cookbook-signing)
|
||||
- [React Native Performance](cookbook-react-native)
|
36
docs.wrm/cookbook/react-native.wrm
Normal file
36
docs.wrm/cookbook/react-native.wrm
Normal file
@ -0,0 +1,36 @@
|
||||
_section: React Native @<cookbook-react-native>
|
||||
|
||||
When using React Native, many of the built-in cryptographic primitives
|
||||
can be replaced by native, substantially faster implementations.
|
||||
|
||||
This should be available in its own package in the future, but for now
|
||||
this is highly recommended, and requires installing the
|
||||
[[link-npm-react-native-quick-crypto]] package.
|
||||
|
||||
|
||||
_code:
|
||||
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import crypto from "react-native-quick-crypto";
|
||||
|
||||
ethers.randomBytes.register((length) => {
|
||||
return new Uint8Array(crypto.randomBytes(length));
|
||||
});
|
||||
|
||||
ethers.computeHmac.register((algo, key, data) => {
|
||||
return crypto.createHmac(algo, key).update(data).digest();
|
||||
});
|
||||
|
||||
ethers.pbkdf2.register((passwd, salt, iter, keylen, algo) => {
|
||||
return crypto.pbkdf2Sync(passwd, salt, iter, keylen, algo);
|
||||
});
|
||||
|
||||
ethers.sha256.register((data) => {
|
||||
return crypto.createHash('sha256').update(data).digest();
|
||||
});
|
||||
|
||||
ethers.sha512.register((data) => {
|
||||
return crypto.createHash('sha512').update(data).digest();
|
||||
});
|
||||
|
273
docs.wrm/cookbook/signing.wrm
Normal file
273
docs.wrm/cookbook/signing.wrm
Normal file
@ -0,0 +1,273 @@
|
||||
_section: Signing @<cookbook-signing>
|
||||
|
||||
Signing content and providing the content and signature to a
|
||||
Contract allows on-chain validation that a signer has access
|
||||
to the private key of a specific address.
|
||||
|
||||
The ecrecover algorithm allows the public key to be determined
|
||||
given some message digest and the signature generated by the
|
||||
private key for that digest. From the public key, the address
|
||||
can then be computed.
|
||||
|
||||
How a digest is derived depends on the type of data being
|
||||
signed and a variety of encoding formats are employed. Each
|
||||
format is designed to ensure that they do not collide, so for
|
||||
example, a user **cannot** be tricked into signing a message
|
||||
which is actually a valid transaction.
|
||||
|
||||
For this reason, most APIs in Ethereum do not permit signing a
|
||||
raw digest, and instead require a separate API for each format
|
||||
type and require the related data be specified, protecting the
|
||||
user from accidentally authorizing an action they didn't intend.
|
||||
|
||||
_subsection: Messages @<cookbook-signing-messages>
|
||||
|
||||
A signed message can be any data, bu it is generally recommended
|
||||
to use human-readable text, as this is easier for a user to
|
||||
verify visually.
|
||||
|
||||
This technique could be used, for example, to sign into a service
|
||||
by using the text ``"I am signing into ethers.org on 2023-06-04 12:57pm"``.
|
||||
The user can then see the message in MetaMask or on a Ledger
|
||||
Hardware Wallet and accept that they wish to sign the message which
|
||||
the site can then autheticate them with. By providing a timestamp
|
||||
the site can ensure that an older signed message cannot be used again
|
||||
in the future.
|
||||
|
||||
The format that is signed uses [[link-eip-191]] with the
|
||||
**personal sign** version code (``0x45``, or ``"E"``).
|
||||
|
||||
For those interested in the choice of this prefix, signed messages
|
||||
began as a Bitcoin feature, which used ``"\\x18Bitcoin Signed Message:\\n"``,
|
||||
which was a Bitcoin var-int length-prefixed string (as ``0x18`` is 24,
|
||||
the length of ``"Bitcoin Signed Message:\\n"``.). When Ethereum adopted
|
||||
the similar feature, the relevant string was ``"\\x19Ethereum Signed Message:\\n"``.
|
||||
|
||||
In one of the most brilliant instances of technical retcon-ing,
|
||||
since 0x19 is invalid as the first byte of a transaction (in [[link-rlp]] it
|
||||
indicates a single byte of value 25), the initial byte ``\\x19`` has
|
||||
now been adopted as a prefix for //some sort of signed data//,
|
||||
where the second byte determines how to interpret that data. If the
|
||||
second bytes is 69 (the letter ``"E"``, as in
|
||||
``"Ethereum Signed Message:\\n"``), then the format is a
|
||||
the above prefixed message format.
|
||||
|
||||
So, all existing messages, tools and instances using the signed
|
||||
message format were already EIP-191 compliant, long before the
|
||||
standard existed or was even conceived and allowed for an extensible
|
||||
format for future formats (of which there now a few).
|
||||
|
||||
Anyways, the necessary JavaScript and Solidity are provided below.
|
||||
|
||||
_code: JavaScript @lang<javascript>
|
||||
|
||||
// The contract below is deployed to Sepolia at this address
|
||||
contractAddress = "0xf554DA5e35b2e40C09DDB481545A395da1736513";
|
||||
contract = new Contract(contractAddress, [
|
||||
"function recoverStringFromCompact(string message, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)",
|
||||
"function recoverStringFromExpanded(string message, (uint8 v, bytes32 r, bytes32 s) sig) pure returns (address)",
|
||||
"function recoverStringFromVRS(string message, uint8 v, bytes32 r, bytes32 s) pure returns (address)",
|
||||
"function recoverStringFromRaw(string message, bytes sig) pure returns (address)",
|
||||
"function recoverHashFromCompact(bytes32 hash, (bytes32 r, bytes32 yParityAndS) sig) pure returns (address)"
|
||||
], new ethers.InfuraProvider("sepolia"));
|
||||
|
||||
// The Signer; it does not need to be connected to a Provider to sign
|
||||
signer = new Wallet(id("foobar"));
|
||||
signer.address
|
||||
//_result:
|
||||
|
||||
// Our message
|
||||
message = "Hello World";
|
||||
|
||||
// The raw signature; 65 bytes
|
||||
rawSig = await signer.signMessage(message);
|
||||
//_result:
|
||||
|
||||
// Converting it to a Signature object provides more
|
||||
// flexibility, such as using it as a struct
|
||||
sig = Signature.from(rawSig);
|
||||
//_result:
|
||||
|
||||
|
||||
// If the signature matches the EIP-2098 format, a Signature
|
||||
// can be passed as the struct value directly, since the
|
||||
// parser will pull out the matching struct keys from sig.
|
||||
await contract.recoverStringFromCompact(message, sig);
|
||||
//_result:
|
||||
|
||||
// Likewise, if the struct keys match an expanded signature
|
||||
// struct, it can also be passed as the struct value directly.
|
||||
await contract.recoverStringFromExpanded(message, sig);
|
||||
//_result:
|
||||
|
||||
// If using an older API which requires the v, r and s be passed
|
||||
// separately, those members are present on the Signature.
|
||||
await contract.recoverStringFromVRS(message, sig.v, sig.r, sig.s);
|
||||
//_result:
|
||||
|
||||
// Or if using a an API that expects a raw signature.
|
||||
await contract.recoverStringFromRaw(message, rawSig);
|
||||
//_result:
|
||||
|
||||
// Note: The above recovered addresses matches the signer address
|
||||
|
||||
_null:
|
||||
|
||||
The Solidity Contract has been deployed and verified on
|
||||
the Sepolia testnet at the address
|
||||
[0xf554DA5e35b2e40C09DDB481545A395da1736513](link-sol-recovermessage).
|
||||
|
||||
It provides a variety of examples using various Signature
|
||||
encodings and formats, to recover the address for an [[link-eip-191]]
|
||||
signed message.
|
||||
|
||||
_code: Solidity @lang<solidity>
|
||||
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// For more info, see: https://docs.ethers.org
|
||||
|
||||
|
||||
pragma solidity ^0.8.21;
|
||||
|
||||
// Returns the decimal string representation of value
|
||||
function itoa(uint value) pure returns (string memory) {
|
||||
|
||||
// Count the length of the decimal string representation
|
||||
uint length = 1;
|
||||
uint v = value;
|
||||
while ((v /= 10) != 0) { length++; }
|
||||
|
||||
// Allocated enough bytes
|
||||
bytes memory result = new bytes(length);
|
||||
|
||||
// Place each ASCII string character in the string,
|
||||
// right to left
|
||||
while (true) {
|
||||
length--;
|
||||
|
||||
// The ASCII value of the modulo 10 value
|
||||
result[length] = bytes1(uint8(0x30 + (value % 10)));
|
||||
|
||||
value /= 10;
|
||||
|
||||
if (length == 0) { break; }
|
||||
}
|
||||
|
||||
return string(result);
|
||||
}
|
||||
|
||||
contract RecoverMessage {
|
||||
|
||||
// This is the EIP-2098 compact representation, which reduces gas costs
|
||||
struct SignatureCompact {
|
||||
bytes32 r;
|
||||
bytes32 yParityAndS;
|
||||
}
|
||||
|
||||
// This is an expaned Signature representation
|
||||
struct SignatureExpanded {
|
||||
uint8 v;
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
}
|
||||
|
||||
// Helper function
|
||||
function _ecrecover(string memory message, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
|
||||
// Compute the EIP-191 prefixed message
|
||||
bytes memory prefixedMessage = abi.encodePacked(
|
||||
"\x19Ethereum Signed Message:\n",
|
||||
itoa(bytes(message).length),
|
||||
message
|
||||
);
|
||||
|
||||
// Compute the message digest
|
||||
bytes32 digest = keccak256(prefixedMessage);
|
||||
|
||||
// Use the native ecrecover provided by the EVM
|
||||
return ecrecover(digest, v, r, s);
|
||||
}
|
||||
|
||||
// Recover the address from an EIP-2098 compact Signature, which packs the bit for
|
||||
// v into an unused bit within s, which saves gas overall, costing a little extra
|
||||
// in computation, but saves far more in calldata length.
|
||||
//
|
||||
// This Signature format is 64 bytes in length.
|
||||
function recoverStringFromCompact(string calldata message, SignatureCompact calldata sig) public pure returns (address) {
|
||||
|
||||
// Decompose the EIP-2098 signature (the struct is 64 bytes in length)
|
||||
uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255);
|
||||
bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1);
|
||||
|
||||
return _ecrecover(message, v, sig.r, s);
|
||||
}
|
||||
|
||||
// Recover the address from the an expanded Signature struct.
|
||||
//
|
||||
// This Signature format is 96 bytes in length.
|
||||
function recoverStringFromExpanded(string calldata message, SignatureExpanded calldata sig) public pure returns (address) {
|
||||
|
||||
// The v, r and s are included directly within the struct, which is 96 bytes in length
|
||||
return _ecrecover(message, sig.v, sig.r, sig.s);
|
||||
}
|
||||
|
||||
// Recover the address from a v, r and s passed directly into the method.
|
||||
//
|
||||
// This Signature format is 96 bytes in length.
|
||||
function recoverStringFromVRS(string calldata message, uint8 v, bytes32 r, bytes32 s) public pure returns (address) {
|
||||
|
||||
// The v, r and s are included directly within the struct, which is 96 bytes in length
|
||||
return _ecrecover(message, v, r, s);
|
||||
}
|
||||
|
||||
// Recover the address from a raw signature. The signature is 65 bytes, which when
|
||||
// ABI encoded is 160 bytes long (a pointer, a length and the padded 3 words of data).
|
||||
//
|
||||
// When using raw signatures, some tools return the v as 0 or 1. In this case you must
|
||||
// add 27 to that value as v must be either 27 or 28.
|
||||
//
|
||||
// This Signature format is 65 bytes of data, but when ABI encoded is 160 bytes in length;
|
||||
// a pointer (32 bytes), a length (32 bytes) and the padded 3 words of data (96 bytes).
|
||||
function recoverStringFromRaw(string calldata message, bytes calldata sig) public pure returns (address) {
|
||||
|
||||
// Sanity check before using assembly
|
||||
require(sig.length == 65, "invalid signature");
|
||||
|
||||
// Decompose the raw signature into r, s and v (note the order)
|
||||
uint8 v;
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
assembly {
|
||||
r := calldataload(sig.offset)
|
||||
s := calldataload(add(sig.offset, 0x20))
|
||||
v := calldataload(add(sig.offset, 0x21))
|
||||
}
|
||||
|
||||
return _ecrecover(message, v, r, s);
|
||||
}
|
||||
|
||||
// This is provided as a quick example for those that only need to recover a signature
|
||||
// for a signed hash (highly discouraged; but common), which means we can hardcode the
|
||||
// length in the prefix. This means we can drop the itoa and _ecrecover functions above.
|
||||
function recoverHashFromCompact(bytes32 hash, SignatureCompact calldata sig) public pure returns (address) {
|
||||
bytes memory prefixedMessage = abi.encodePacked(
|
||||
// Notice the length of the message is hard-coded to 32
|
||||
// here -----------------------v
|
||||
"\x19Ethereum Signed Message:\n32",
|
||||
hash
|
||||
);
|
||||
|
||||
bytes32 digest = keccak256(prefixedMessage);
|
||||
|
||||
// Decompose the EIP-2098 signature
|
||||
uint8 v = 27 + uint8(uint256(sig.yParityAndS) >> 255);
|
||||
bytes32 s = bytes32((uint256(sig.yParityAndS) << 1) >> 1);
|
||||
|
||||
return ecrecover(digest, v, sig.r, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_subsection: EIP-712 Typed Data @<cookbook-signing-eip712>
|
||||
|
||||
//Coming soon...//
|
@ -2,3 +2,4 @@ link-npm-ethers [link-ethers-npm](https://www.npmjs.com/search?q=%40ethersprojec
|
||||
link-npm-events [EventEmitter](https://nodejs.org/dist/latest-v13.x/docs/api/events.html#events_class_eventemitter)
|
||||
link-npm-query-bignumber [link-npm-query-bignumber](https://www.npmjs.com/search?q=bignumber)
|
||||
link-npm-react-native-get-random-values [React Native get-random-values](https://www.npmjs.com/package/react-native-get-random-values)
|
||||
link-npm-react-native-quick-crypto [Quick Crypto](https://www.npmjs.com/package/react-native-quick-crypto)
|
||||
|
@ -45,3 +45,5 @@ link-solidity-errors [link-solidity-errors](https://docs.soliditylang.org/en/v0.
|
||||
link-solidity-events [link-solidity-events](https://docs.soliditylang.org/en/v0.8.4/abi-spec.html#events)
|
||||
|
||||
link-other-ethereum-dev-docs [link-other-ethereum-dev-docs](https://ethereum.org/en/developers/docs/)
|
||||
|
||||
link-sol-recovermessage [RecoverMessage.sol](https://sepolia.etherscan.io/address/0xf554da5e35b2e40c09ddb481545a395da1736513#code)
|
||||
|
Loading…
Reference in New Issue
Block a user