-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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")); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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...// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters