wakumo / eth-sig-util Goto Github PK
View Code? Open in Web Editor NEWEthereum signature utility porting from JS
License: MIT License
Ethereum signature utility porting from JS
License: MIT License
Specifically how the 'types' are mapped to valid Json for a TypedDataVersion.V4 call?
E/flutter ( 7237): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Invalid argument(s): jsonData format is not corresponding to TypedMessage
E/flutter ( 7237): #0 TypedDataUtil.hashMessage (package:eth_sig_util/util/typed_data.dart:45:9)
E/flutter ( 7237): #1 EthSigUtil.signTypedData (package:eth_sig_util/eth_sig_util.dart:26:27)
An example of the typed data I am trying to sign is on step three of this page https://docs.rarible.org/asset/creating-an-asset#erc721-lazy-minting
I have this, but I don't know how to format it properly for the signTypedData 'types' field.
"types": {
"EIP712Domain" [
{
type: "string",
name: "name",
},
{
type: "string",
name: "version",
},
{
type: "uint256",
name: "chainId",
},
{
type: "address",
name: "verifyingContract",
}
],
"Mint721": [
{ name: "tokenId", type: "uint256" },
{ name: "tokenURI", type: "string" },
{ name: "creators", type: "Part[]" },
{ name: "royalties", type: "Part[]" }
],
"Part": [
{ name: "account", type: "address" },
{ name: "value", type: "uint96" }
]
},
Thanks for your time. Excited to see this package.
While debugging #13, I recreated the flow to use USDC's transferWithAuthorization
with node and @metamask/eth-sig-util. The same flow/code in dart does not generate the same signature.
JS
Here is a working js example that generates a signature like:
0xeef120842a7c11630aa1758d4291c2f36006afd813b2342570fcec9c12bc91970f57a6996ebc37ce006b100e7a93474d4add11c919ec05141342c8334e187fc71b
, which I have successfully submitted to Ropsten.
const ethSigUtil = require("@metamask/eth-sig-util");
const { SignTypedDataVersion } = require("@metamask/eth-sig-util");
const usdcAddress = '0x07865c6e87b9f70255377e024ace6630c1eaa37f';
const privateKey = '04...100';
const pkBuffer = Buffer.from(privateKey, "hex");
const fromAddress = '0xd9d3768621774c3c357b9b5fcb23b5735ea2c7ae';
const toAddress = '0x33eeCb4A75C9241eEa0190e5dCDFb76D2Ed67613';
const amount = 1000000;
const validAfter = 0;
const validBefore = Math.floor(Date.now() / 1000) + 3600;
const nonce = web3.utils.randomHex(32);
const msgParams = {
domain: {
chainId: '3',
name: 'USD Coin',
verifyingContract: usdcAddress,
version: '2',
},
message: {
from: fromAddress,
to: toAddress,
value: amount,
validAfter: validAfter,
validBefore: validBefore,
nonce: nonce
},
primaryType: 'TransferWithAuthorization',
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" }
]
},
};
let version = SignTypedDataVersion.V4;
const signature = ethSigUtil.signTypedData({privateKey: pkBuffer, data: msgParams, version: version});
console.log("Signature:" + signature);
Dart
Trying to emulate this same pattern in dart (using the same nonce and validBefore) generates a different and invalid signature:
0x150c45911e31162551b31dd7e50eb3104c6eef84d9d01c3405eb831ffd575d0b7809cb65d91b8adf8cd059508c6a075146c118c7df2415537103b888ebc969921b
, which comes back with invalid signature
from the contract call.
import 'package:eth_sig_util/eth_sig_util.dart';
const privateKey = '04...100';
const usdcAddress = '0x07865c6e87b9f70255377e024ace6630c1eaa37f';
const fromAddress = '0xd9d3768621774c3c357b9b5fcb23b5735ea2c7ae';
const toAddress = '0x33eeCb4A75C9241eEa0190e5dCDFb76D2Ed67613';
const amount = 1000000;
const validAfter = 0;
const validBefore = 1646931849; // logged from the js code
const nonce = "0xc150256aaf6f21cae3ce7f44804afa079681caa4dd86d1f0d9c05fe5f1a824ea"; // logged from the js code
const usdcAddress = '0x07865c6e87b9f70255377e024ace6630c1eaa37f';
final msgParams = {
"domain": {
"chainId": '3',
"name": 'USD Coin',
"verifyingContract": usdcAddress,
"version": '2',
},
"message": {
"from": fromAddress,
"to": toAddress,
"value": amount,
"validAfter": validAfter,
"validBefore": validBefore,
"nonce": nonce
},
"primaryType": 'TransferWithAuthorization',
"types": {
"EIP712Domain": [
{"name": 'name', "type": 'string'},
{"name": 'version', "type": 'string'},
{"name": 'chainId', "type": 'uint256'},
{"name": 'verifyingContract', "type": 'address'},
],
"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"}
]
},
};
final jsonData = jsonEncode(msgParams);
String signature = EthSigUtil.signTypedData(privateKey: privateKey, jsonData: jsonData, version: TypedDataVersion.V4);
print("Signature: " + signature);
Why does this not work as expected?
final private =
EthPrivateKey.fromHex(WalletSp.instance.getCurrentPrivateKey());
final data = 'hello ๅๅ';
final sign1 = EthSigUtil.signPersonalMessage(privateKey: bytesToHex(private.privateKey),message: Uint8List.fromList(utf8.encode(data)));
logger('sign1:$sign1');
I reported this error by calling the signPersonalMessage function
The library only takes a hex string for a private key and it requires to convert this string to an Uint8List whenever it calls a sign or recover.
Suggestion 1: Add a required named argument to all methods that takes a private key and change a string private key to optional. If an optional private key is set, then simply change this to bytes and call this method again.
Suggestion 2: Add a separate method that takes a private key as an Uint8List.
same address gets success with other chain
other address gets success with call chains
When debugging #13 I used one of the test cases as a starting point:
const privateKey = '4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0';
const json =
r'''{"types":{"EIP712Domain":[{"type":"string","name":"name"},{"type":"string","name":"version"},{"type":"uint256","name":"chainId"},{"type":"address","name":"verifyingContract"}],"Part":[{"name":"account","type":"address"},{"name":"value","type":"uint96"}],"Mint721":[{"name":"tokenId","type":"uint256"},{"name":"tokenURI","type":"string"},{"name":"creators","type":"Part[]"},{"name":"royalties","type":"Part[]"}]},"domain":{"name":"Mint721","version":"1","chainId":4,"verifyingContract":"0x2547760120aed692eb19d22a5d9ccfe0f7872fce"},"primaryType":"Mint721","message":{"@type":"ERC721","contract":"0x2547760120aed692eb19d22a5d9ccfe0f7872fce","tokenId":"1","uri":"ipfs://ipfs/hash","creators":[{"account":"0xc5eac3488524d577a1495492599e8013b1f91efa","value":10000}],"royalties":[],"tokenURI":"ipfs://ipfs/hash"}}''';
test('should sign data with custom type which has an array', () {
final signature = EthSigUtil.signTypedData(privateKey: privateKey, jsonData: json, version: TypedDataVersion.V4);
expect(signature,'0x2ce14898e255b8d1e5f296a293548607720951e507a5416a0515baef0420984f2e28df8824206db9dbab0e7f5b14eeb834d48ada4444e5f15e7bfd777d2069481c');
});
This works as expected, but when adding the chainId parameter to the same test, and checking the sigParams:
String signature = EthSigUtil.signTypedData(privateKey: privateKey, jsonData: jsonTest, version: TypedDataVersion.V4, chainId: 280);
var sigParams = SignatureUtil.fromRpcSig(signature);
The result is an invalid signature length: "ArgumentError (Invalid argument(s): Invalid signature length)"
So something seems to be broken with the chainId
implementation.
When I try my code in #13 without the chainID, the signature length is correct, but I'm not sure if leaving the chainID out is going to lead to problems? (EDIT: the signature is still invalid, see #16)
EthSigUtil.ecRecover() works well with a normal signed message though
I am hitting a compatibility issue migrating from v.0.0.5 to v0.0.6 since I do not require a salt value in my EIP720Domain. Setting the required salt value to null does not work.
I found if I omitted the salt: null output in the toJson method the downstream libraries reverted to the prior V0.0.5 behavior.
There is likely a more null aware operator way to code this, but here is the logic:
Map<String, dynamic> toJson() {
if (salt != null) {
return <String, dynamic>{
'name': name,
'version': version,
'chainId': chainId,
'salt': salt,
'verifyingContract': verifyingContract
};
}
else {
// Omit the null salt entry
return
<String, dynamic>{
'name': name,
'version': version,
'chainId': chainId,
'verifyingContract': verifyingContract
};
}
I want to use EthSigUtil.signMessage
using privateKeyInBytes
parameter but, in the source code, it's not used when calling SignatureUtil.sign
.
Source code:
static String signMessage(
{String? privateKey,
Uint8List? privateKeyInBytes,
required Uint8List message}) {
return SignatureUtil.sign(message: message, privateKey: privateKey);
}
Edit: After looking at the source code of many functions, a looooot of them are not using privateKeyInBytes
.
This package doesn't work with messages that have several strings (large messages), most possible due to dart uses UTF-16.
It's limited in the practical usability, unless there is a example, that can assist with this. I tried several.
I have tried to use the other method it provides, which is the signTypedData V1 version, and that works. However the EthSigUtil.ecRecover method to verify and get public address, doesn't support that [signedTypeData].
So for verification/get public address, isn't possible. It would be great if it did, as it would stop this issue.
Hope, I can get assistance with this, thanks.
eth-sig-util/lib/util/signature.dart
Line 212 in 199ca24
I'm attempting to use the transferWithAuthorization
function of USDC. I'm able to sign the typedData but when submitting it with web3.eth.sendSignedTransaction I get the following error:
transaction could not be decoded: could not decode RLP components: insufficient remaining input for short list
or sometimes
transaction could not be decoded: could not decode RLP components: extra data at end
Here is what I use to construct the typedData:
final Map<String, dynamic> rawTypedData = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"}
]
},
"domain": {
"name": "USDC",
"version": "2",
"chainId": config.chain.chainId,
"verifyingContract": config.chain.usdcContract
},
"primaryType": "TransferWithAuthorization",
"message": {
"from": user.pubKey,
"to": ethAddress.toString(),
"value": amount.toString(),
"validAfter": BigInt.zero.toString(),
"validBefore": BigInt.from(9223372036854775807).toString(),
"nonce": "0x" + Nonce.secure(32).toString()
}
};
And I sign it with:
String signedData = EthSigUtil.signTypedData(
privateKey: bytesToHex(wallet.privateKey.privateKey),
jsonData: jsonEncode(rawTypedData),
version: TypedDataVersion.V4);
Finding it hard to debug this, not sure what the error message refers to, or if it's even accurate (the tiniest change in the typedData seems to give slightly different errors)
The print of rawTypedData.toString() is:
{
types: {
EIP712Domain: [
{name: name, type: string},
{name: version, type: string},
{name: chainId, type: uint256},
{name: verifyingContract, type: address}],
TransferWithAuthorization: [
{name: from, type: address},
{name: to, type: address},
{name: value, type: uint256},
{name: validAfter, type: uint256},
{name: validBefore, type: uint256},
{name: nonce, type: bytes32}]
},
domain: {
name: USDC,
version: 2,
chainId: 3,
verifyingContract: 0x07865c6e87b9f70255377e024ace6630c1eaa37f
},
primaryType: TransferWithAuthorization,
message: {
from: 0xfe55e0a42c29d4502631b9123ba585430e1f9cdc,
to: 0x33eecb4a75c9241eea0190e5dcdfb76d2ed67613,
value: 1000000,
validAfter: 0,
validBefore: 9223372036854775807,
nonce: 0x0f8Vf33cD9ZaLGgZi6c68SbDC5fC5s9xenmiNF8M3H3D20SgWFe3Y8Yqt8m4sJ2w
}
}
I am using EthSigUtil.signPersonalMessage and EthSigUtil.recoverPersonalSignature
I am importing the following libraries
import 'package:web3dart/web3dart.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:collection/collection.dart';
import 'package:web3dart/crypto.dart';
Following is the code for generating the KeyPair. I am using the package:web3dart/web3dart.dart for generating the KeyPairs
var random_number = Random.secure();
EthPrivateKey keyPair = EthPrivateKey.createRandom(random_number);
I ran the signing and verifying multiple times in a loop.
When I run over 100 times, I get 1-2 verifications which fail. And this error rate is proportional when I run it over 1000 times too..Roughly 1-2% verification failures. I get Invalid argument(s): invalid signature
error
This is my signing logic
Future<String> sign (final String message) async
{
List<int> message_list = message.codeUnits;
final Uint8List bytes_message = Uint8List
.fromList(message_list);
String signature = EthSigUtil.signPersonalMessage
(
privateKey: bytesToHex(keyPair.privateKey),
message: bytes_message
);
return signature;
}
And the code for verify is this :
Future verify (final String message, final String signature, String public_key) async
{
String recovered_address = '';
List<int> message_list = message.codeUnits;
final Uint8List bytes_message = Uint8List
.fromList(message_list);
try {
recovered_address = EthSigUtil.recoverPersonalSignature
(
signature: signature,
message: bytes_message
);
}
catch (e)
{
return false;
}
var given_address = publicKeyToAddress(hexToBytes(public_key));
bool is_verified = const ListEquality()
.equals(
hexToBytes(recovered_address),
given_address
);
return is_verified;
}
Am I doing something wrong?
I am not seeing the results I expect when I compare signature request signed by metamask and the plugin. Here is a failing unit test with example data, how I am using it, and the result I expect.
Thank you for your time on this!
import 'dart:convert';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/model/typed_data.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
JsonEncoder encoder = JsonEncoder.withIndent(' ');
String? prettyprint;
// Keys for unit testing only
String walletAddress = '0xbdB75073F7bC2E2fe26F846975b3ff64fd30D3b4';
String walletPrivateKey =
'dcb9f84b32289b5079d58d0c8231d10e8c8cef7034493a91aa1b185068fdd6c8';
// // Create lazyMintRequestBody part 1
Map<String, dynamic> messageToSign = <String, dynamic>{
"@type": "ERC721",
"contract": "0xB0EA149212Eb707a1E5FC1D2d3fD318a8d94cf05",
"tokenId":
"85811016661282950678352305265629525770441762754136673725069232823650475835400",
"uri": "/ipfs/QmVUzkLxEoCRyit8uXAuUoVUgFw1c7Uvz7T4bkGgJUwxcf",
"creators": [
{"account": walletAddress, "value": 10000}
],
"royalties": <int>[],
"tokenURI": "/ipfs/QmVUzkLxEoCRyit8uXAuUoVUgFw1c7Uvz7T4bkGgJUwxcf",
};
prettyprint = encoder.convert(messageToSign);
//print('messageToSign---\n$prettyprint\n---');
final TypedMessage rawTypedData = TypedMessage(
types: {
"EIP712Domain": [
TypedDataField(name: "name", type: "string"),
TypedDataField(name: "version", type: "string"),
TypedDataField(name: "chainId", type: "uint256"),
TypedDataField(name: "verifyingContract", type: "address")
],
"Mint721": [
TypedDataField(name: "tokenId", type: "uint256"),
TypedDataField(name: "tokenURI", type: "string"),
TypedDataField(name: "uri", type: "string"),
TypedDataField(name: "creators", type: "Part[]"),
TypedDataField(name: "royalties", type: "Part[]")
],
"Part": [
TypedDataField(name: "account", type: "address"),
TypedDataField(name: "value", type: "uint96"),
]
},
domain: EIP712Domain(
name: "Mint721",
version: "1",
chainId: 3,
verifyingContract: "0xB0EA149212Eb707a1E5FC1D2d3fD318a8d94cf05"),
primaryType: "Mint721",
message: messageToSign,
);
prettyprint = encoder.convert(rawTypedData);
print('rawTypedData---\n$prettyprint\n---');
/// Sign the typed Data Structure of request
String jsonData = jsonEncode(rawTypedData);
String signature = '';
test('test signature value of typed data', () {
signature = EthSigUtil.signTypedData(
privateKey: walletPrivateKey,
jsonData: jsonData,
version: TypedDataVersion.V4,
chainId: 3);
expect(signature,
'0xa275d9322e77f0b9680393028363d6d5ede89149e2e1c2931cfbc318ddc1496b7cee8bee4dbd632d859ebdd0bd7f2343ddcd5c2b2768b3c512963c49f1800fe11c');
});
}
// from = "0xbdB75073F7bC2E2fe26F846975b3ff64fd30D3b4"
// SignedDataRequest (msgData)
//{"types":{"EIP712Domain":[{"type":"string","name":"name"},{"type":"string","name":"version"},{"type":"uint256","name":"chainId"},{"type":"address","name":"verifyingContract"}],"Part":[{"name":"account","type":"address"},{"name":"value","type":"uint96"}],"Mint721":[{"name":"tokenId","type":"uint256"},{"name":"tokenURI","type":"string"},{"name":"uri","type":"string"},{"name":"creators","type":"Part[]"},{"name":"royalties","type":"Part[]"}]},"domain":{"name":"Mint721","version":"1","chainId":3,"verifyingContract":"0xB0EA149212Eb707a1E5FC1D2d3fD318a8d94cf05"},"primaryType":"Mint721","message":{"@type":"ERC721","contract":"0xB0EA149212Eb707a1E5FC1D2d3fD318a8d94cf05","tokenId":"85811016661282950678352305265629525770441762754136673725069232823650475835400","uri":"/ipfs/QmVUzkLxEoCRyit8uXAuUoVUgFw1c7Uvz7T4bkGgJUwxcf","creators":[{"account":"0xbdB75073F7bC2E2fe26F846975b3ff64fd30D3b4","value":10000}],"royalties":[],"tokenURI":"/ipfs/QmVUzkLxEoCRyit8uXAuUoVUgFw1c7Uvz7T4bkGgJUwxcf"}}
// const sig = await provider.send("eth_signTypedData_v4", [from, msgData]);
// Provider returned
// sig : 0xa275d9322e77f0b9680393028363d6d5ede89149e2e1c2931cfbc318ddc1496b7cee8bee4dbd632d859ebdd0bd7f2343ddcd5c2b2768b3c512963c49f1800fe11c
// r: 0xa275d9322e77f0b9680393028363d6d5ede89149e2e1c2931cfbc318ddc1496b , s: 0x7cee8bee4dbd632d859ebdd0bd7f2343ddcd5c2b2768b3c512963c49f1800fe1 , v: 28
Hi,
I have a issue when i try to encode bool type with solidityPack method. I tried to dig out on the source code and found that issue.
I've created this (PR) that works for me right now,
Please kindly review!
I'm attempting to use USDC's transferWithAuthorization
on a Dart client for submission by a Node.js backend.
My understanding of the flow so far:
etc-sig-util
transferWithAuthorization
web3dart
I am currently stuck on step 4, where ethClient.signTransaction gives the following error:
RPCError: got code 3 with msg "execution reverted: ECRecover: invalid signature 'v' value".
v
is from sigParams.v
, but cast to a BigInt
as otherwise the Web3Dart.Transaction.callContract
threw a type error.
Any help figuring out the flow and getting this to work would be appreciated.
Full code:
Future<String?> signUSDC(BigInt amount, Web3Dart.EthereumAddress ethAddress, User user) async {
final config = ref.read(configurationProvider).instance;
final wallet =
Web3Dart.Wallet.fromJson(user.ethereumWallet!, kWalletPassword);
final validAfter = BigInt.zero;
final validBefore = BigInt.from(9223372036854775807);
final nonce = Nonce.secure(32);
final nonceBytes = utf8.encode(nonce.toString());
// 1. Create typed data
final Map<String, dynamic> rawTypedData = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"TransferWithAuthorization": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "validAfter", "type": "uint256"},
{"name": "validBefore", "type": "uint256"},
{"name": "nonce", "type": "bytes32"}
]
},
"domain": {
"name": "USDC",
"version": "2",
"chainId": config.chain.chainId,
"verifyingContract": config.chain.usdcContract
},
"primaryType": "TransferWithAuthorization",
"message": {
"from": user.pubKey,
"to": ethAddress.toString(),
"value": amount.toInt(),
"validAfter": validAfter.toInt(),
"validBefore": validBefore.toInt(),
"nonce": nonceBytes.toString()
}
};
// 2. Sign typed data
String signature = EthSigUtil.signTypedData(
privateKey: bytesToHex(wallet.privateKey.privateKey),
jsonData: jsonEncode(rawTypedData),
version: TypedDataVersion.V4,
chainId: config.chain.chainId);
final contract = await ref.read(usdcContractProvider.future);
var sigParams = SignatureUtil.fromRpcSig(signature);
// 3. Create transaction calling 'transferWithAuthorization'
final transaction = Web3Dart.Transaction.callContract(
contract: contract,
function: contract.function('transferWithAuthorization'),
parameters: [
Web3Dart.EthereumAddress.fromHex(user.pubKey!),
ethAddress,
amount,
validAfter,
validBefore,
nonceBytes,
BigInt.from(sigParams.v),
bigIntToUint8List(sigParams.r),
bigIntToUint8List(sigParams.s)
],
);
// 4. Sign transaction
final signedTx = await ethClient.signTransaction(
wallet.privateKey,
transaction,
chainId: config.chain.chainId,
fetchChainIdFromNetworkId: false);
final hexTx = "0x" + hex.encode(signedTx); // not sure if signedTx or hexTx should be used
// 5. submit signed transaction to backend, get hash in return
try {
final body = json.encode((await defaultParams)
..addAll({
"serializedTx": signedTx,
"validationCode": "[redacted]"
}));
return sendRequest(baseUrl.asUri / '[redacted_endpoint]',
method: DataRequestMethod.POST,
body: body,
omitDefaultParams: false, onSuccess: (map) {
return map['hash'] as String;
}, onError: (error) {
return null;
});
} catch (error) {
return null;
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.