@0xsequence/account 1.1.15 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/0xsequence-account.cjs.dev.js +171 -6
- package/dist/0xsequence-account.cjs.prod.js +171 -6
- package/dist/0xsequence-account.esm.js +172 -7
- package/dist/declarations/src/account.d.ts +10 -4
- package/dist/declarations/src/signer.d.ts +36 -0
- package/package.json +10 -10
- package/src/account.ts +19 -7
- package/src/signer.ts +221 -0
- package/src/utils.ts +14 -0
|
@@ -24,6 +24,166 @@ function _extends() {
|
|
|
24
24
|
return _extends.apply(this, arguments);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isPromise(value) {
|
|
28
|
+
return !!value && typeof value.then === 'function';
|
|
29
|
+
}
|
|
30
|
+
function isDeferrable(value) {
|
|
31
|
+
// The value is deferrable if any of the properties is a Promises
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
return Object.keys(value).some(key => isPromise(value[key]));
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function encodeGasRefundTransaction(option) {
|
|
39
|
+
if (!option) return [];
|
|
40
|
+
const value = ethers.ethers.BigNumber.from(option.value);
|
|
41
|
+
switch (option.token.type) {
|
|
42
|
+
case relayer.proto.FeeTokenType.UNKNOWN:
|
|
43
|
+
return [{
|
|
44
|
+
delegateCall: false,
|
|
45
|
+
revertOnError: true,
|
|
46
|
+
gasLimit: option.gasLimit,
|
|
47
|
+
to: option.to,
|
|
48
|
+
value: value.toHexString(),
|
|
49
|
+
data: []
|
|
50
|
+
}];
|
|
51
|
+
case relayer.proto.FeeTokenType.ERC20_TOKEN:
|
|
52
|
+
if (!option.token.contractAddress) {
|
|
53
|
+
throw new Error(`No contract address for ERC-20 fee option`);
|
|
54
|
+
}
|
|
55
|
+
return [{
|
|
56
|
+
delegateCall: false,
|
|
57
|
+
revertOnError: true,
|
|
58
|
+
gasLimit: option.gasLimit,
|
|
59
|
+
to: option.token.contractAddress,
|
|
60
|
+
value: 0,
|
|
61
|
+
data: new ethers.ethers.utils.Interface([{
|
|
62
|
+
"constant": false,
|
|
63
|
+
"inputs": [{
|
|
64
|
+
"type": "address"
|
|
65
|
+
}, {
|
|
66
|
+
"type": "uint256"
|
|
67
|
+
}],
|
|
68
|
+
"name": "transfer",
|
|
69
|
+
"outputs": [],
|
|
70
|
+
"type": "function"
|
|
71
|
+
}]).encodeFunctionData('transfer', [option.to, value.toHexString()])
|
|
72
|
+
}];
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Unhandled fee token type ${option.token.type}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class AccountSigner {
|
|
78
|
+
constructor(account, chainId, options) {
|
|
79
|
+
this._isSigner = true;
|
|
80
|
+
this.account = account;
|
|
81
|
+
this.chainId = chainId;
|
|
82
|
+
this.options = options;
|
|
83
|
+
}
|
|
84
|
+
get provider() {
|
|
85
|
+
return this.account.providerFor(this.chainId);
|
|
86
|
+
}
|
|
87
|
+
async getAddress() {
|
|
88
|
+
return this.account.address;
|
|
89
|
+
}
|
|
90
|
+
signMessage(message) {
|
|
91
|
+
var _this$options$cantVal, _this$options;
|
|
92
|
+
return this.account.signMessage(message, this.chainId, (_this$options$cantVal = (_this$options = this.options) == null ? void 0 : _this$options.cantValidateBehavior) != null ? _this$options$cantVal : 'throw');
|
|
93
|
+
}
|
|
94
|
+
async defaultSelectFee(_txs, options) {
|
|
95
|
+
// If no options, return undefined
|
|
96
|
+
if (options.length === 0) return undefined;
|
|
97
|
+
|
|
98
|
+
// If there are multiple options, try them one by one
|
|
99
|
+
// until we find one that satisfies the balance requirement
|
|
100
|
+
const balanceOfAbi = [{
|
|
101
|
+
"constant": true,
|
|
102
|
+
"inputs": [{
|
|
103
|
+
"type": "address"
|
|
104
|
+
}],
|
|
105
|
+
"name": "balanceOf",
|
|
106
|
+
"outputs": [{
|
|
107
|
+
"type": "uint256"
|
|
108
|
+
}],
|
|
109
|
+
"type": "function"
|
|
110
|
+
}];
|
|
111
|
+
for (const option of options) {
|
|
112
|
+
if (option.token.type === relayer.proto.FeeTokenType.UNKNOWN) {
|
|
113
|
+
// Native token
|
|
114
|
+
const balance = await this.getBalance();
|
|
115
|
+
if (balance.gte(ethers.ethers.BigNumber.from(option.value))) {
|
|
116
|
+
return option;
|
|
117
|
+
}
|
|
118
|
+
} else if (option.token.contractAddress && option.token.type === relayer.proto.FeeTokenType.ERC20_TOKEN) {
|
|
119
|
+
// ERC20 token
|
|
120
|
+
const token = new ethers.ethers.Contract(option.token.contractAddress, balanceOfAbi, this.provider);
|
|
121
|
+
const balance = await token.balanceOf(this.account.address);
|
|
122
|
+
if (balance.gte(ethers.ethers.BigNumber.from(option.value))) {
|
|
123
|
+
return option;
|
|
124
|
+
}
|
|
125
|
+
} else ;
|
|
126
|
+
}
|
|
127
|
+
throw new Error("No fee option available - not enough balance");
|
|
128
|
+
}
|
|
129
|
+
async sendTransaction(txsPromise) {
|
|
130
|
+
var _this$options$stubSig, _this$options2, _this$options$selectF, _this$options3, _this$options4;
|
|
131
|
+
const txs = isDeferrable(txsPromise) ? await ethers.ethers.utils.resolveProperties(txsPromise) : txsPromise;
|
|
132
|
+
const prepare = await this.account.prepareTransactions({
|
|
133
|
+
txs,
|
|
134
|
+
chainId: this.chainId,
|
|
135
|
+
stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map()
|
|
136
|
+
});
|
|
137
|
+
const selectMethod = (_this$options$selectF = (_this$options3 = this.options) == null ? void 0 : _this$options3.selectFee) != null ? _this$options$selectF : this.defaultSelectFee.bind(this);
|
|
138
|
+
const feeOption = await selectMethod(txs, prepare.feeOptions);
|
|
139
|
+
const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)];
|
|
140
|
+
return this.account.sendTransaction(finalTransactions, this.chainId, prepare.feeQuote, undefined, undefined, (_this$options4 = this.options) != null && _this$options4.nonceSpace ? {
|
|
141
|
+
nonceSpace: this.options.nonceSpace
|
|
142
|
+
} : undefined);
|
|
143
|
+
}
|
|
144
|
+
getBalance(blockTag) {
|
|
145
|
+
return this.provider.getBalance(this.account.address, blockTag);
|
|
146
|
+
}
|
|
147
|
+
call(transaction, blockTag) {
|
|
148
|
+
return this.provider.call(transaction, blockTag);
|
|
149
|
+
}
|
|
150
|
+
async resolveName(name) {
|
|
151
|
+
const res = await this.provider.resolveName(name);
|
|
152
|
+
if (!res) throw new Error(`Could not resolve name ${name}`);
|
|
153
|
+
return res;
|
|
154
|
+
}
|
|
155
|
+
connect(_provider) {
|
|
156
|
+
throw new Error("Method not implemented.");
|
|
157
|
+
}
|
|
158
|
+
signTransaction(transaction) {
|
|
159
|
+
throw new Error("Method not implemented.");
|
|
160
|
+
}
|
|
161
|
+
getTransactionCount(blockTag) {
|
|
162
|
+
throw new Error("Method not implemented.");
|
|
163
|
+
}
|
|
164
|
+
estimateGas(transaction) {
|
|
165
|
+
throw new Error("Method not implemented.");
|
|
166
|
+
}
|
|
167
|
+
getChainId() {
|
|
168
|
+
return Promise.resolve(ethers.ethers.BigNumber.from(this.chainId).toNumber());
|
|
169
|
+
}
|
|
170
|
+
getGasPrice() {
|
|
171
|
+
throw new Error("Method not implemented.");
|
|
172
|
+
}
|
|
173
|
+
getFeeData() {
|
|
174
|
+
throw new Error("Method not implemented.");
|
|
175
|
+
}
|
|
176
|
+
checkTransaction(transaction) {
|
|
177
|
+
throw new Error("Method not implemented.");
|
|
178
|
+
}
|
|
179
|
+
populateTransaction(transaction) {
|
|
180
|
+
throw new Error("Method not implemented.");
|
|
181
|
+
}
|
|
182
|
+
_checkProvider(operation) {
|
|
183
|
+
throw new Error("Method not implemented.");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
27
187
|
class Chain0Reader {
|
|
28
188
|
async isDeployed(_wallet) {
|
|
29
189
|
return false;
|
|
@@ -51,6 +211,9 @@ class Account {
|
|
|
51
211
|
this.migrations = options.migrations || migration.defaults.DefaultMigrations;
|
|
52
212
|
this.migrator = new migration.migrator.Migrator(options.tracker, this.migrations, this.contexts);
|
|
53
213
|
}
|
|
214
|
+
getSigner(chainId, options) {
|
|
215
|
+
return new AccountSigner(this, chainId, options);
|
|
216
|
+
}
|
|
54
217
|
static async new(options) {
|
|
55
218
|
var _options$migrations;
|
|
56
219
|
const mig = new migration.migrator.Migrator(options.tracker, (_options$migrations = options.migrations) != null ? _options$migrations : migration.defaults.DefaultMigrations, options.contexts);
|
|
@@ -92,7 +255,7 @@ class Account {
|
|
|
92
255
|
if (!found) throw new Error(`Network not found for chainId ${chainId}`);
|
|
93
256
|
return found;
|
|
94
257
|
}
|
|
95
|
-
|
|
258
|
+
providerFor(chainId) {
|
|
96
259
|
const found = this.network(chainId);
|
|
97
260
|
if (!found.provider && !found.rpcUrl) throw new Error(`Provider not found for chainId ${chainId}`);
|
|
98
261
|
return found.provider || new ethers.ethers.providers.StaticJsonRpcProvider(utils.getDefaultConnectionInfo(found.rpcUrl), {
|
|
@@ -105,7 +268,7 @@ class Account {
|
|
|
105
268
|
|
|
106
269
|
// TODO: Networks should be able to provide a reader directly
|
|
107
270
|
// and we should default to the on-chain reader
|
|
108
|
-
return new core.commons.reader.OnChainReader(this.
|
|
271
|
+
return new core.commons.reader.OnChainReader(this.providerFor(chainId));
|
|
109
272
|
}
|
|
110
273
|
relayer(chainId) {
|
|
111
274
|
const found = this.network(chainId);
|
|
@@ -452,11 +615,13 @@ class Account {
|
|
|
452
615
|
signMessage(message, chainId, cantValidateBehavior = 'ignore') {
|
|
453
616
|
return this.signDigest(ethers.ethers.utils.keccak256(message), chainId, true, cantValidateBehavior);
|
|
454
617
|
}
|
|
455
|
-
async signTransactions(txs, chainId, pstatus) {
|
|
618
|
+
async signTransactions(txs, chainId, pstatus, options) {
|
|
456
619
|
const status = pstatus || (await this.status(chainId));
|
|
457
620
|
this.mustBeFullyMigrated(status);
|
|
458
621
|
const wallet = this.walletForStatus(chainId, status);
|
|
459
|
-
const signed = await wallet.signTransactions(txs)
|
|
622
|
+
const signed = await wallet.signTransactions(txs, (options == null ? void 0 : options.nonceSpace) && {
|
|
623
|
+
space: options == null ? void 0 : options.nonceSpace
|
|
624
|
+
});
|
|
460
625
|
return _extends({}, signed, {
|
|
461
626
|
signature: await this.decorateSignature(signed.signature, status)
|
|
462
627
|
});
|
|
@@ -589,10 +754,10 @@ class Account {
|
|
|
589
754
|
feeQuote: gasRefundQuote.quote
|
|
590
755
|
};
|
|
591
756
|
}
|
|
592
|
-
async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback) {
|
|
757
|
+
async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback, options) {
|
|
593
758
|
const status = await this.status(chainId);
|
|
594
759
|
const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId);
|
|
595
|
-
const signed = await this.signTransactions(predecorated, chainId);
|
|
760
|
+
const signed = await this.signTransactions(predecorated, chainId, undefined, options);
|
|
596
761
|
// TODO: is it safe to pass status again here?
|
|
597
762
|
return this.sendSignedTransactions(signed, chainId, quote, undefined, callback);
|
|
598
763
|
}
|
|
@@ -24,6 +24,166 @@ function _extends() {
|
|
|
24
24
|
return _extends.apply(this, arguments);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isPromise(value) {
|
|
28
|
+
return !!value && typeof value.then === 'function';
|
|
29
|
+
}
|
|
30
|
+
function isDeferrable(value) {
|
|
31
|
+
// The value is deferrable if any of the properties is a Promises
|
|
32
|
+
if (typeof value === "object") {
|
|
33
|
+
return Object.keys(value).some(key => isPromise(value[key]));
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function encodeGasRefundTransaction(option) {
|
|
39
|
+
if (!option) return [];
|
|
40
|
+
const value = ethers.ethers.BigNumber.from(option.value);
|
|
41
|
+
switch (option.token.type) {
|
|
42
|
+
case relayer.proto.FeeTokenType.UNKNOWN:
|
|
43
|
+
return [{
|
|
44
|
+
delegateCall: false,
|
|
45
|
+
revertOnError: true,
|
|
46
|
+
gasLimit: option.gasLimit,
|
|
47
|
+
to: option.to,
|
|
48
|
+
value: value.toHexString(),
|
|
49
|
+
data: []
|
|
50
|
+
}];
|
|
51
|
+
case relayer.proto.FeeTokenType.ERC20_TOKEN:
|
|
52
|
+
if (!option.token.contractAddress) {
|
|
53
|
+
throw new Error(`No contract address for ERC-20 fee option`);
|
|
54
|
+
}
|
|
55
|
+
return [{
|
|
56
|
+
delegateCall: false,
|
|
57
|
+
revertOnError: true,
|
|
58
|
+
gasLimit: option.gasLimit,
|
|
59
|
+
to: option.token.contractAddress,
|
|
60
|
+
value: 0,
|
|
61
|
+
data: new ethers.ethers.utils.Interface([{
|
|
62
|
+
"constant": false,
|
|
63
|
+
"inputs": [{
|
|
64
|
+
"type": "address"
|
|
65
|
+
}, {
|
|
66
|
+
"type": "uint256"
|
|
67
|
+
}],
|
|
68
|
+
"name": "transfer",
|
|
69
|
+
"outputs": [],
|
|
70
|
+
"type": "function"
|
|
71
|
+
}]).encodeFunctionData('transfer', [option.to, value.toHexString()])
|
|
72
|
+
}];
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Unhandled fee token type ${option.token.type}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class AccountSigner {
|
|
78
|
+
constructor(account, chainId, options) {
|
|
79
|
+
this._isSigner = true;
|
|
80
|
+
this.account = account;
|
|
81
|
+
this.chainId = chainId;
|
|
82
|
+
this.options = options;
|
|
83
|
+
}
|
|
84
|
+
get provider() {
|
|
85
|
+
return this.account.providerFor(this.chainId);
|
|
86
|
+
}
|
|
87
|
+
async getAddress() {
|
|
88
|
+
return this.account.address;
|
|
89
|
+
}
|
|
90
|
+
signMessage(message) {
|
|
91
|
+
var _this$options$cantVal, _this$options;
|
|
92
|
+
return this.account.signMessage(message, this.chainId, (_this$options$cantVal = (_this$options = this.options) == null ? void 0 : _this$options.cantValidateBehavior) != null ? _this$options$cantVal : 'throw');
|
|
93
|
+
}
|
|
94
|
+
async defaultSelectFee(_txs, options) {
|
|
95
|
+
// If no options, return undefined
|
|
96
|
+
if (options.length === 0) return undefined;
|
|
97
|
+
|
|
98
|
+
// If there are multiple options, try them one by one
|
|
99
|
+
// until we find one that satisfies the balance requirement
|
|
100
|
+
const balanceOfAbi = [{
|
|
101
|
+
"constant": true,
|
|
102
|
+
"inputs": [{
|
|
103
|
+
"type": "address"
|
|
104
|
+
}],
|
|
105
|
+
"name": "balanceOf",
|
|
106
|
+
"outputs": [{
|
|
107
|
+
"type": "uint256"
|
|
108
|
+
}],
|
|
109
|
+
"type": "function"
|
|
110
|
+
}];
|
|
111
|
+
for (const option of options) {
|
|
112
|
+
if (option.token.type === relayer.proto.FeeTokenType.UNKNOWN) {
|
|
113
|
+
// Native token
|
|
114
|
+
const balance = await this.getBalance();
|
|
115
|
+
if (balance.gte(ethers.ethers.BigNumber.from(option.value))) {
|
|
116
|
+
return option;
|
|
117
|
+
}
|
|
118
|
+
} else if (option.token.contractAddress && option.token.type === relayer.proto.FeeTokenType.ERC20_TOKEN) {
|
|
119
|
+
// ERC20 token
|
|
120
|
+
const token = new ethers.ethers.Contract(option.token.contractAddress, balanceOfAbi, this.provider);
|
|
121
|
+
const balance = await token.balanceOf(this.account.address);
|
|
122
|
+
if (balance.gte(ethers.ethers.BigNumber.from(option.value))) {
|
|
123
|
+
return option;
|
|
124
|
+
}
|
|
125
|
+
} else ;
|
|
126
|
+
}
|
|
127
|
+
throw new Error("No fee option available - not enough balance");
|
|
128
|
+
}
|
|
129
|
+
async sendTransaction(txsPromise) {
|
|
130
|
+
var _this$options$stubSig, _this$options2, _this$options$selectF, _this$options3, _this$options4;
|
|
131
|
+
const txs = isDeferrable(txsPromise) ? await ethers.ethers.utils.resolveProperties(txsPromise) : txsPromise;
|
|
132
|
+
const prepare = await this.account.prepareTransactions({
|
|
133
|
+
txs,
|
|
134
|
+
chainId: this.chainId,
|
|
135
|
+
stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map()
|
|
136
|
+
});
|
|
137
|
+
const selectMethod = (_this$options$selectF = (_this$options3 = this.options) == null ? void 0 : _this$options3.selectFee) != null ? _this$options$selectF : this.defaultSelectFee.bind(this);
|
|
138
|
+
const feeOption = await selectMethod(txs, prepare.feeOptions);
|
|
139
|
+
const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)];
|
|
140
|
+
return this.account.sendTransaction(finalTransactions, this.chainId, prepare.feeQuote, undefined, undefined, (_this$options4 = this.options) != null && _this$options4.nonceSpace ? {
|
|
141
|
+
nonceSpace: this.options.nonceSpace
|
|
142
|
+
} : undefined);
|
|
143
|
+
}
|
|
144
|
+
getBalance(blockTag) {
|
|
145
|
+
return this.provider.getBalance(this.account.address, blockTag);
|
|
146
|
+
}
|
|
147
|
+
call(transaction, blockTag) {
|
|
148
|
+
return this.provider.call(transaction, blockTag);
|
|
149
|
+
}
|
|
150
|
+
async resolveName(name) {
|
|
151
|
+
const res = await this.provider.resolveName(name);
|
|
152
|
+
if (!res) throw new Error(`Could not resolve name ${name}`);
|
|
153
|
+
return res;
|
|
154
|
+
}
|
|
155
|
+
connect(_provider) {
|
|
156
|
+
throw new Error("Method not implemented.");
|
|
157
|
+
}
|
|
158
|
+
signTransaction(transaction) {
|
|
159
|
+
throw new Error("Method not implemented.");
|
|
160
|
+
}
|
|
161
|
+
getTransactionCount(blockTag) {
|
|
162
|
+
throw new Error("Method not implemented.");
|
|
163
|
+
}
|
|
164
|
+
estimateGas(transaction) {
|
|
165
|
+
throw new Error("Method not implemented.");
|
|
166
|
+
}
|
|
167
|
+
getChainId() {
|
|
168
|
+
return Promise.resolve(ethers.ethers.BigNumber.from(this.chainId).toNumber());
|
|
169
|
+
}
|
|
170
|
+
getGasPrice() {
|
|
171
|
+
throw new Error("Method not implemented.");
|
|
172
|
+
}
|
|
173
|
+
getFeeData() {
|
|
174
|
+
throw new Error("Method not implemented.");
|
|
175
|
+
}
|
|
176
|
+
checkTransaction(transaction) {
|
|
177
|
+
throw new Error("Method not implemented.");
|
|
178
|
+
}
|
|
179
|
+
populateTransaction(transaction) {
|
|
180
|
+
throw new Error("Method not implemented.");
|
|
181
|
+
}
|
|
182
|
+
_checkProvider(operation) {
|
|
183
|
+
throw new Error("Method not implemented.");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
27
187
|
class Chain0Reader {
|
|
28
188
|
async isDeployed(_wallet) {
|
|
29
189
|
return false;
|
|
@@ -51,6 +211,9 @@ class Account {
|
|
|
51
211
|
this.migrations = options.migrations || migration.defaults.DefaultMigrations;
|
|
52
212
|
this.migrator = new migration.migrator.Migrator(options.tracker, this.migrations, this.contexts);
|
|
53
213
|
}
|
|
214
|
+
getSigner(chainId, options) {
|
|
215
|
+
return new AccountSigner(this, chainId, options);
|
|
216
|
+
}
|
|
54
217
|
static async new(options) {
|
|
55
218
|
var _options$migrations;
|
|
56
219
|
const mig = new migration.migrator.Migrator(options.tracker, (_options$migrations = options.migrations) != null ? _options$migrations : migration.defaults.DefaultMigrations, options.contexts);
|
|
@@ -92,7 +255,7 @@ class Account {
|
|
|
92
255
|
if (!found) throw new Error(`Network not found for chainId ${chainId}`);
|
|
93
256
|
return found;
|
|
94
257
|
}
|
|
95
|
-
|
|
258
|
+
providerFor(chainId) {
|
|
96
259
|
const found = this.network(chainId);
|
|
97
260
|
if (!found.provider && !found.rpcUrl) throw new Error(`Provider not found for chainId ${chainId}`);
|
|
98
261
|
return found.provider || new ethers.ethers.providers.StaticJsonRpcProvider(utils.getDefaultConnectionInfo(found.rpcUrl), {
|
|
@@ -105,7 +268,7 @@ class Account {
|
|
|
105
268
|
|
|
106
269
|
// TODO: Networks should be able to provide a reader directly
|
|
107
270
|
// and we should default to the on-chain reader
|
|
108
|
-
return new core.commons.reader.OnChainReader(this.
|
|
271
|
+
return new core.commons.reader.OnChainReader(this.providerFor(chainId));
|
|
109
272
|
}
|
|
110
273
|
relayer(chainId) {
|
|
111
274
|
const found = this.network(chainId);
|
|
@@ -452,11 +615,13 @@ class Account {
|
|
|
452
615
|
signMessage(message, chainId, cantValidateBehavior = 'ignore') {
|
|
453
616
|
return this.signDigest(ethers.ethers.utils.keccak256(message), chainId, true, cantValidateBehavior);
|
|
454
617
|
}
|
|
455
|
-
async signTransactions(txs, chainId, pstatus) {
|
|
618
|
+
async signTransactions(txs, chainId, pstatus, options) {
|
|
456
619
|
const status = pstatus || (await this.status(chainId));
|
|
457
620
|
this.mustBeFullyMigrated(status);
|
|
458
621
|
const wallet = this.walletForStatus(chainId, status);
|
|
459
|
-
const signed = await wallet.signTransactions(txs)
|
|
622
|
+
const signed = await wallet.signTransactions(txs, (options == null ? void 0 : options.nonceSpace) && {
|
|
623
|
+
space: options == null ? void 0 : options.nonceSpace
|
|
624
|
+
});
|
|
460
625
|
return _extends({}, signed, {
|
|
461
626
|
signature: await this.decorateSignature(signed.signature, status)
|
|
462
627
|
});
|
|
@@ -589,10 +754,10 @@ class Account {
|
|
|
589
754
|
feeQuote: gasRefundQuote.quote
|
|
590
755
|
};
|
|
591
756
|
}
|
|
592
|
-
async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback) {
|
|
757
|
+
async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback, options) {
|
|
593
758
|
const status = await this.status(chainId);
|
|
594
759
|
const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId);
|
|
595
|
-
const signed = await this.signTransactions(predecorated, chainId);
|
|
760
|
+
const signed = await this.signTransactions(predecorated, chainId, undefined, options);
|
|
596
761
|
// TODO: is it safe to pass status again here?
|
|
597
762
|
return this.sendSignedTransactions(signed, chainId, quote, undefined, callback);
|
|
598
763
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { commons, universal } from '@0xsequence/core';
|
|
2
2
|
import { defaults, migrator, version } from '@0xsequence/migration';
|
|
3
|
-
import { isRelayer, RpcRelayer } from '@0xsequence/relayer';
|
|
3
|
+
import { proto, isRelayer, RpcRelayer } from '@0xsequence/relayer';
|
|
4
4
|
import { getDefaultConnectionInfo, encodeTypedDataDigest } from '@0xsequence/utils';
|
|
5
5
|
import { Wallet } from '@0xsequence/wallet';
|
|
6
6
|
import { ethers } from 'ethers';
|
|
@@ -20,6 +20,166 @@ function _extends() {
|
|
|
20
20
|
return _extends.apply(this, arguments);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function isPromise(value) {
|
|
24
|
+
return !!value && typeof value.then === 'function';
|
|
25
|
+
}
|
|
26
|
+
function isDeferrable(value) {
|
|
27
|
+
// The value is deferrable if any of the properties is a Promises
|
|
28
|
+
if (typeof value === "object") {
|
|
29
|
+
return Object.keys(value).some(key => isPromise(value[key]));
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function encodeGasRefundTransaction(option) {
|
|
35
|
+
if (!option) return [];
|
|
36
|
+
const value = ethers.BigNumber.from(option.value);
|
|
37
|
+
switch (option.token.type) {
|
|
38
|
+
case proto.FeeTokenType.UNKNOWN:
|
|
39
|
+
return [{
|
|
40
|
+
delegateCall: false,
|
|
41
|
+
revertOnError: true,
|
|
42
|
+
gasLimit: option.gasLimit,
|
|
43
|
+
to: option.to,
|
|
44
|
+
value: value.toHexString(),
|
|
45
|
+
data: []
|
|
46
|
+
}];
|
|
47
|
+
case proto.FeeTokenType.ERC20_TOKEN:
|
|
48
|
+
if (!option.token.contractAddress) {
|
|
49
|
+
throw new Error(`No contract address for ERC-20 fee option`);
|
|
50
|
+
}
|
|
51
|
+
return [{
|
|
52
|
+
delegateCall: false,
|
|
53
|
+
revertOnError: true,
|
|
54
|
+
gasLimit: option.gasLimit,
|
|
55
|
+
to: option.token.contractAddress,
|
|
56
|
+
value: 0,
|
|
57
|
+
data: new ethers.utils.Interface([{
|
|
58
|
+
"constant": false,
|
|
59
|
+
"inputs": [{
|
|
60
|
+
"type": "address"
|
|
61
|
+
}, {
|
|
62
|
+
"type": "uint256"
|
|
63
|
+
}],
|
|
64
|
+
"name": "transfer",
|
|
65
|
+
"outputs": [],
|
|
66
|
+
"type": "function"
|
|
67
|
+
}]).encodeFunctionData('transfer', [option.to, value.toHexString()])
|
|
68
|
+
}];
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unhandled fee token type ${option.token.type}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class AccountSigner {
|
|
74
|
+
constructor(account, chainId, options) {
|
|
75
|
+
this._isSigner = true;
|
|
76
|
+
this.account = account;
|
|
77
|
+
this.chainId = chainId;
|
|
78
|
+
this.options = options;
|
|
79
|
+
}
|
|
80
|
+
get provider() {
|
|
81
|
+
return this.account.providerFor(this.chainId);
|
|
82
|
+
}
|
|
83
|
+
async getAddress() {
|
|
84
|
+
return this.account.address;
|
|
85
|
+
}
|
|
86
|
+
signMessage(message) {
|
|
87
|
+
var _this$options$cantVal, _this$options;
|
|
88
|
+
return this.account.signMessage(message, this.chainId, (_this$options$cantVal = (_this$options = this.options) == null ? void 0 : _this$options.cantValidateBehavior) != null ? _this$options$cantVal : 'throw');
|
|
89
|
+
}
|
|
90
|
+
async defaultSelectFee(_txs, options) {
|
|
91
|
+
// If no options, return undefined
|
|
92
|
+
if (options.length === 0) return undefined;
|
|
93
|
+
|
|
94
|
+
// If there are multiple options, try them one by one
|
|
95
|
+
// until we find one that satisfies the balance requirement
|
|
96
|
+
const balanceOfAbi = [{
|
|
97
|
+
"constant": true,
|
|
98
|
+
"inputs": [{
|
|
99
|
+
"type": "address"
|
|
100
|
+
}],
|
|
101
|
+
"name": "balanceOf",
|
|
102
|
+
"outputs": [{
|
|
103
|
+
"type": "uint256"
|
|
104
|
+
}],
|
|
105
|
+
"type": "function"
|
|
106
|
+
}];
|
|
107
|
+
for (const option of options) {
|
|
108
|
+
if (option.token.type === proto.FeeTokenType.UNKNOWN) {
|
|
109
|
+
// Native token
|
|
110
|
+
const balance = await this.getBalance();
|
|
111
|
+
if (balance.gte(ethers.BigNumber.from(option.value))) {
|
|
112
|
+
return option;
|
|
113
|
+
}
|
|
114
|
+
} else if (option.token.contractAddress && option.token.type === proto.FeeTokenType.ERC20_TOKEN) {
|
|
115
|
+
// ERC20 token
|
|
116
|
+
const token = new ethers.Contract(option.token.contractAddress, balanceOfAbi, this.provider);
|
|
117
|
+
const balance = await token.balanceOf(this.account.address);
|
|
118
|
+
if (balance.gte(ethers.BigNumber.from(option.value))) {
|
|
119
|
+
return option;
|
|
120
|
+
}
|
|
121
|
+
} else ;
|
|
122
|
+
}
|
|
123
|
+
throw new Error("No fee option available - not enough balance");
|
|
124
|
+
}
|
|
125
|
+
async sendTransaction(txsPromise) {
|
|
126
|
+
var _this$options$stubSig, _this$options2, _this$options$selectF, _this$options3, _this$options4;
|
|
127
|
+
const txs = isDeferrable(txsPromise) ? await ethers.utils.resolveProperties(txsPromise) : txsPromise;
|
|
128
|
+
const prepare = await this.account.prepareTransactions({
|
|
129
|
+
txs,
|
|
130
|
+
chainId: this.chainId,
|
|
131
|
+
stubSignatureOverrides: (_this$options$stubSig = (_this$options2 = this.options) == null ? void 0 : _this$options2.stubSignatureOverrides) != null ? _this$options$stubSig : new Map()
|
|
132
|
+
});
|
|
133
|
+
const selectMethod = (_this$options$selectF = (_this$options3 = this.options) == null ? void 0 : _this$options3.selectFee) != null ? _this$options$selectF : this.defaultSelectFee.bind(this);
|
|
134
|
+
const feeOption = await selectMethod(txs, prepare.feeOptions);
|
|
135
|
+
const finalTransactions = [...prepare.transactions, ...encodeGasRefundTransaction(feeOption)];
|
|
136
|
+
return this.account.sendTransaction(finalTransactions, this.chainId, prepare.feeQuote, undefined, undefined, (_this$options4 = this.options) != null && _this$options4.nonceSpace ? {
|
|
137
|
+
nonceSpace: this.options.nonceSpace
|
|
138
|
+
} : undefined);
|
|
139
|
+
}
|
|
140
|
+
getBalance(blockTag) {
|
|
141
|
+
return this.provider.getBalance(this.account.address, blockTag);
|
|
142
|
+
}
|
|
143
|
+
call(transaction, blockTag) {
|
|
144
|
+
return this.provider.call(transaction, blockTag);
|
|
145
|
+
}
|
|
146
|
+
async resolveName(name) {
|
|
147
|
+
const res = await this.provider.resolveName(name);
|
|
148
|
+
if (!res) throw new Error(`Could not resolve name ${name}`);
|
|
149
|
+
return res;
|
|
150
|
+
}
|
|
151
|
+
connect(_provider) {
|
|
152
|
+
throw new Error("Method not implemented.");
|
|
153
|
+
}
|
|
154
|
+
signTransaction(transaction) {
|
|
155
|
+
throw new Error("Method not implemented.");
|
|
156
|
+
}
|
|
157
|
+
getTransactionCount(blockTag) {
|
|
158
|
+
throw new Error("Method not implemented.");
|
|
159
|
+
}
|
|
160
|
+
estimateGas(transaction) {
|
|
161
|
+
throw new Error("Method not implemented.");
|
|
162
|
+
}
|
|
163
|
+
getChainId() {
|
|
164
|
+
return Promise.resolve(ethers.BigNumber.from(this.chainId).toNumber());
|
|
165
|
+
}
|
|
166
|
+
getGasPrice() {
|
|
167
|
+
throw new Error("Method not implemented.");
|
|
168
|
+
}
|
|
169
|
+
getFeeData() {
|
|
170
|
+
throw new Error("Method not implemented.");
|
|
171
|
+
}
|
|
172
|
+
checkTransaction(transaction) {
|
|
173
|
+
throw new Error("Method not implemented.");
|
|
174
|
+
}
|
|
175
|
+
populateTransaction(transaction) {
|
|
176
|
+
throw new Error("Method not implemented.");
|
|
177
|
+
}
|
|
178
|
+
_checkProvider(operation) {
|
|
179
|
+
throw new Error("Method not implemented.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
23
183
|
class Chain0Reader {
|
|
24
184
|
async isDeployed(_wallet) {
|
|
25
185
|
return false;
|
|
@@ -47,6 +207,9 @@ class Account {
|
|
|
47
207
|
this.migrations = options.migrations || defaults.DefaultMigrations;
|
|
48
208
|
this.migrator = new migrator.Migrator(options.tracker, this.migrations, this.contexts);
|
|
49
209
|
}
|
|
210
|
+
getSigner(chainId, options) {
|
|
211
|
+
return new AccountSigner(this, chainId, options);
|
|
212
|
+
}
|
|
50
213
|
static async new(options) {
|
|
51
214
|
var _options$migrations;
|
|
52
215
|
const mig = new migrator.Migrator(options.tracker, (_options$migrations = options.migrations) != null ? _options$migrations : defaults.DefaultMigrations, options.contexts);
|
|
@@ -88,7 +251,7 @@ class Account {
|
|
|
88
251
|
if (!found) throw new Error(`Network not found for chainId ${chainId}`);
|
|
89
252
|
return found;
|
|
90
253
|
}
|
|
91
|
-
|
|
254
|
+
providerFor(chainId) {
|
|
92
255
|
const found = this.network(chainId);
|
|
93
256
|
if (!found.provider && !found.rpcUrl) throw new Error(`Provider not found for chainId ${chainId}`);
|
|
94
257
|
return found.provider || new ethers.providers.StaticJsonRpcProvider(getDefaultConnectionInfo(found.rpcUrl), {
|
|
@@ -101,7 +264,7 @@ class Account {
|
|
|
101
264
|
|
|
102
265
|
// TODO: Networks should be able to provide a reader directly
|
|
103
266
|
// and we should default to the on-chain reader
|
|
104
|
-
return new commons.reader.OnChainReader(this.
|
|
267
|
+
return new commons.reader.OnChainReader(this.providerFor(chainId));
|
|
105
268
|
}
|
|
106
269
|
relayer(chainId) {
|
|
107
270
|
const found = this.network(chainId);
|
|
@@ -448,11 +611,13 @@ class Account {
|
|
|
448
611
|
signMessage(message, chainId, cantValidateBehavior = 'ignore') {
|
|
449
612
|
return this.signDigest(ethers.utils.keccak256(message), chainId, true, cantValidateBehavior);
|
|
450
613
|
}
|
|
451
|
-
async signTransactions(txs, chainId, pstatus) {
|
|
614
|
+
async signTransactions(txs, chainId, pstatus, options) {
|
|
452
615
|
const status = pstatus || (await this.status(chainId));
|
|
453
616
|
this.mustBeFullyMigrated(status);
|
|
454
617
|
const wallet = this.walletForStatus(chainId, status);
|
|
455
|
-
const signed = await wallet.signTransactions(txs)
|
|
618
|
+
const signed = await wallet.signTransactions(txs, (options == null ? void 0 : options.nonceSpace) && {
|
|
619
|
+
space: options == null ? void 0 : options.nonceSpace
|
|
620
|
+
});
|
|
456
621
|
return _extends({}, signed, {
|
|
457
622
|
signature: await this.decorateSignature(signed.signature, status)
|
|
458
623
|
});
|
|
@@ -585,10 +750,10 @@ class Account {
|
|
|
585
750
|
feeQuote: gasRefundQuote.quote
|
|
586
751
|
};
|
|
587
752
|
}
|
|
588
|
-
async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback) {
|
|
753
|
+
async sendTransaction(txs, chainId, quote, skipPreDecorate = false, callback, options) {
|
|
589
754
|
const status = await this.status(chainId);
|
|
590
755
|
const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId);
|
|
591
|
-
const signed = await this.signTransactions(predecorated, chainId);
|
|
756
|
+
const signed = await this.signTransactions(predecorated, chainId, undefined, options);
|
|
592
757
|
// TODO: is it safe to pass status again here?
|
|
593
758
|
return this.sendSignedTransactions(signed, chainId, quote, undefined, callback);
|
|
594
759
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { commons } from '@0xsequence/core';
|
|
2
2
|
import { migrator } from '@0xsequence/migration';
|
|
3
|
-
import { NetworkConfig } from '@0xsequence/network';
|
|
3
|
+
import { ChainId, NetworkConfig } from '@0xsequence/network';
|
|
4
4
|
import { FeeOption, FeeQuote, Relayer } from '@0xsequence/relayer';
|
|
5
5
|
import { tracker } from '@0xsequence/sessions';
|
|
6
6
|
import { Orchestrator } from '@0xsequence/signhub';
|
|
7
7
|
import { Wallet } from '@0xsequence/wallet';
|
|
8
8
|
import { ethers, TypedDataDomain, TypedDataField } from 'ethers';
|
|
9
|
+
import { AccountSigner, AccountSignerOptions } from "./signer.js";
|
|
9
10
|
export type AccountStatus = {
|
|
10
11
|
original: {
|
|
11
12
|
version: number;
|
|
@@ -50,6 +51,7 @@ export declare class Account {
|
|
|
50
51
|
readonly migrations: migrator.Migrations;
|
|
51
52
|
private orchestrator;
|
|
52
53
|
constructor(options: AccountOptions);
|
|
54
|
+
getSigner(chainId: ChainId, options?: AccountSignerOptions): AccountSigner;
|
|
53
55
|
static new(options: {
|
|
54
56
|
config: commons.config.SimpleConfig;
|
|
55
57
|
tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker;
|
|
@@ -65,7 +67,7 @@ export declare class Account {
|
|
|
65
67
|
config: commons.config.ConfigCoder;
|
|
66
68
|
};
|
|
67
69
|
network(chainId: ethers.BigNumberish): NetworkConfig;
|
|
68
|
-
|
|
70
|
+
providerFor(chainId: ethers.BigNumberish): ethers.providers.Provider;
|
|
69
71
|
reader(chainId: ethers.BigNumberish): commons.reader.Reader;
|
|
70
72
|
relayer(chainId: ethers.BigNumberish): Relayer;
|
|
71
73
|
setOrchestrator(orchestrator: Orchestrator): void;
|
|
@@ -101,7 +103,9 @@ export declare class Account {
|
|
|
101
103
|
bootstrapTransactions(chainId: ethers.BigNumberish, prestatus?: AccountStatus): Promise<Omit<commons.transaction.IntendedTransactionBundle, 'chainId'>>;
|
|
102
104
|
doBootstrap(chainId: ethers.BigNumberish, feeQuote?: FeeQuote, prestatus?: AccountStatus): Promise<commons.transaction.TransactionResponse<any>>;
|
|
103
105
|
signMessage(message: ethers.BytesLike, chainId: ethers.BigNumberish, cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw'): Promise<string>;
|
|
104
|
-
signTransactions(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, pstatus?: AccountStatus
|
|
106
|
+
signTransactions(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, pstatus?: AccountStatus, options?: {
|
|
107
|
+
nonceSpace?: ethers.BigNumberish;
|
|
108
|
+
}): Promise<commons.transaction.SignedTransactionBundle>;
|
|
105
109
|
signMigrations(chainId: ethers.BigNumberish, editConfig: (prevConfig: commons.config.Config) => commons.config.Config): Promise<boolean>;
|
|
106
110
|
signAllMigrations(editConfig: (prevConfig: commons.config.Config) => commons.config.Config): Promise<{
|
|
107
111
|
signedMigrations: Array<any>;
|
|
@@ -123,7 +127,9 @@ export declare class Account {
|
|
|
123
127
|
chainId: ethers.BigNumberish;
|
|
124
128
|
stubSignatureOverrides: Map<string, string>;
|
|
125
129
|
}): Promise<PreparedTransactions>;
|
|
126
|
-
sendTransaction(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, quote?: FeeQuote, skipPreDecorate?: boolean, callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void
|
|
130
|
+
sendTransaction(txs: commons.transaction.Transactionish, chainId: ethers.BigNumberish, quote?: FeeQuote, skipPreDecorate?: boolean, callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void, options?: {
|
|
131
|
+
nonceSpace?: ethers.BigNumberish;
|
|
132
|
+
}): Promise<ethers.providers.TransactionResponse>;
|
|
127
133
|
signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, message: Record<string, any>, chainId: ethers.BigNumberish, cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw'): Promise<string>;
|
|
128
134
|
getAllSigners(): Promise<{
|
|
129
135
|
address: string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ChainId } from "@0xsequence/network";
|
|
2
|
+
import { Account } from "./account.js";
|
|
3
|
+
import { ethers } from "ethers";
|
|
4
|
+
import { commons } from "@0xsequence/core";
|
|
5
|
+
import { FeeOption } from "@0xsequence/relayer";
|
|
6
|
+
export type AccountSignerOptions = {
|
|
7
|
+
nonceSpace?: ethers.BigNumberish;
|
|
8
|
+
cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw';
|
|
9
|
+
stubSignatureOverrides?: Map<string, string>;
|
|
10
|
+
selectFee?: (txs: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish, options: FeeOption[]) => Promise<FeeOption | undefined>;
|
|
11
|
+
};
|
|
12
|
+
export declare class AccountSigner implements ethers.Signer {
|
|
13
|
+
account: Account;
|
|
14
|
+
chainId: ChainId;
|
|
15
|
+
readonly options?: AccountSignerOptions | undefined;
|
|
16
|
+
readonly _isSigner = true;
|
|
17
|
+
constructor(account: Account, chainId: ChainId, options?: AccountSignerOptions | undefined);
|
|
18
|
+
get provider(): ethers.providers.Provider;
|
|
19
|
+
getAddress(): Promise<string>;
|
|
20
|
+
signMessage(message: string | ethers.utils.Bytes): Promise<string>;
|
|
21
|
+
private defaultSelectFee;
|
|
22
|
+
sendTransaction(txsPromise: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish): Promise<ethers.providers.TransactionResponse>;
|
|
23
|
+
getBalance(blockTag?: ethers.providers.BlockTag | undefined): Promise<ethers.BigNumber>;
|
|
24
|
+
call(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>, blockTag?: ethers.providers.BlockTag | undefined): Promise<string>;
|
|
25
|
+
resolveName(name: string): Promise<string>;
|
|
26
|
+
connect(_provider: ethers.providers.Provider): ethers.Signer;
|
|
27
|
+
signTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<string>;
|
|
28
|
+
getTransactionCount(blockTag?: ethers.providers.BlockTag | undefined): Promise<number>;
|
|
29
|
+
estimateGas(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<ethers.BigNumber>;
|
|
30
|
+
getChainId(): Promise<number>;
|
|
31
|
+
getGasPrice(): Promise<ethers.BigNumber>;
|
|
32
|
+
getFeeData(): Promise<ethers.providers.FeeData>;
|
|
33
|
+
checkTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): ethers.utils.Deferrable<ethers.providers.TransactionRequest>;
|
|
34
|
+
populateTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<ethers.providers.TransactionRequest>;
|
|
35
|
+
_checkProvider(operation?: string | undefined): void;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@0xsequence/account",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "tools for migrating sequence wallets to new versions",
|
|
5
5
|
"repository": "https://github.com/0xsequence/sequence.js/tree/master/packages/account",
|
|
6
6
|
"source": "src/index.ts",
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ethers": "^5.5.2",
|
|
13
|
-
"@0xsequence/core": "1.
|
|
14
|
-
"@0xsequence/migration": "1.
|
|
15
|
-
"@0xsequence/
|
|
16
|
-
"@0xsequence/
|
|
17
|
-
"@0xsequence/
|
|
18
|
-
"@0xsequence/utils": "1.
|
|
19
|
-
"@0xsequence/wallet": "1.
|
|
13
|
+
"@0xsequence/core": "1.2.0",
|
|
14
|
+
"@0xsequence/migration": "1.2.0",
|
|
15
|
+
"@0xsequence/relayer": "1.2.0",
|
|
16
|
+
"@0xsequence/network": "1.2.0",
|
|
17
|
+
"@0xsequence/sessions": "1.2.0",
|
|
18
|
+
"@0xsequence/utils": "1.2.0",
|
|
19
|
+
"@0xsequence/wallet": "1.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
23
23
|
"nyc": "^15.1.0",
|
|
24
|
-
"@0xsequence/signhub": "1.
|
|
25
|
-
"@0xsequence/tests": "1.
|
|
24
|
+
"@0xsequence/signhub": "1.2.0",
|
|
25
|
+
"@0xsequence/tests": "1.2.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src",
|
package/src/account.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { commons, universal } from '@0xsequence/core'
|
|
2
2
|
import { migrator, defaults, version } from '@0xsequence/migration'
|
|
3
|
-
import { NetworkConfig } from '@0xsequence/network'
|
|
3
|
+
import { ChainId, NetworkConfig } from '@0xsequence/network'
|
|
4
4
|
import { FeeOption, FeeQuote, isRelayer, Relayer, RpcRelayer } from '@0xsequence/relayer'
|
|
5
5
|
import { tracker } from '@0xsequence/sessions'
|
|
6
6
|
import { Orchestrator } from '@0xsequence/signhub'
|
|
7
7
|
import { encodeTypedDataDigest, getDefaultConnectionInfo } from '@0xsequence/utils'
|
|
8
8
|
import { Wallet } from '@0xsequence/wallet'
|
|
9
9
|
import { ethers, TypedDataDomain, TypedDataField } from 'ethers'
|
|
10
|
+
import { AccountSigner, AccountSignerOptions } from './signer'
|
|
10
11
|
|
|
11
12
|
export type AccountStatus = {
|
|
12
13
|
original: {
|
|
@@ -106,6 +107,10 @@ export class Account {
|
|
|
106
107
|
this.migrator = new migrator.Migrator(options.tracker, this.migrations, this.contexts)
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
getSigner(chainId: ChainId, options?: AccountSignerOptions): AccountSigner {
|
|
111
|
+
return new AccountSigner(this, chainId, options)
|
|
112
|
+
}
|
|
113
|
+
|
|
109
114
|
static async new(options: {
|
|
110
115
|
config: commons.config.SimpleConfig
|
|
111
116
|
tracker: tracker.ConfigTracker & migrator.PresignedMigrationTracker
|
|
@@ -163,7 +168,7 @@ export class Account {
|
|
|
163
168
|
return found
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
|
|
171
|
+
providerFor(chainId: ethers.BigNumberish): ethers.providers.Provider {
|
|
167
172
|
const found = this.network(chainId)
|
|
168
173
|
if (!found.provider && !found.rpcUrl) throw new Error(`Provider not found for chainId ${chainId}`)
|
|
169
174
|
return (
|
|
@@ -180,7 +185,7 @@ export class Account {
|
|
|
180
185
|
|
|
181
186
|
// TODO: Networks should be able to provide a reader directly
|
|
182
187
|
// and we should default to the on-chain reader
|
|
183
|
-
return new commons.reader.OnChainReader(this.
|
|
188
|
+
return new commons.reader.OnChainReader(this.providerFor(chainId))
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
relayer(chainId: ethers.BigNumberish): Relayer {
|
|
@@ -608,13 +613,16 @@ export class Account {
|
|
|
608
613
|
async signTransactions(
|
|
609
614
|
txs: commons.transaction.Transactionish,
|
|
610
615
|
chainId: ethers.BigNumberish,
|
|
611
|
-
pstatus?: AccountStatus
|
|
616
|
+
pstatus?: AccountStatus,
|
|
617
|
+
options?: {
|
|
618
|
+
nonceSpace?: ethers.BigNumberish
|
|
619
|
+
}
|
|
612
620
|
): Promise<commons.transaction.SignedTransactionBundle> {
|
|
613
621
|
const status = pstatus || (await this.status(chainId))
|
|
614
622
|
this.mustBeFullyMigrated(status)
|
|
615
623
|
|
|
616
624
|
const wallet = this.walletForStatus(chainId, status)
|
|
617
|
-
const signed = await wallet.signTransactions(txs)
|
|
625
|
+
const signed = await wallet.signTransactions(txs, options?.nonceSpace && { space: options?.nonceSpace })
|
|
618
626
|
|
|
619
627
|
return {
|
|
620
628
|
...signed,
|
|
@@ -788,11 +796,15 @@ export class Account {
|
|
|
788
796
|
chainId: ethers.BigNumberish,
|
|
789
797
|
quote?: FeeQuote,
|
|
790
798
|
skipPreDecorate: boolean = false,
|
|
791
|
-
callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void
|
|
799
|
+
callback?: (bundle: commons.transaction.IntendedTransactionBundle) => void,
|
|
800
|
+
options?: {
|
|
801
|
+
nonceSpace?: ethers.BigNumberish
|
|
802
|
+
}
|
|
792
803
|
): Promise<ethers.providers.TransactionResponse> {
|
|
793
804
|
const status = await this.status(chainId)
|
|
805
|
+
|
|
794
806
|
const predecorated = skipPreDecorate ? txs : await this.predecorateTransactions(txs, status, chainId)
|
|
795
|
-
const signed = await this.signTransactions(predecorated, chainId)
|
|
807
|
+
const signed = await this.signTransactions(predecorated, chainId, undefined, options)
|
|
796
808
|
// TODO: is it safe to pass status again here?
|
|
797
809
|
return this.sendSignedTransactions(signed, chainId, quote, undefined, callback)
|
|
798
810
|
}
|
package/src/signer.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ChainId } from "@0xsequence/network"
|
|
2
|
+
import { Account } from "./account"
|
|
3
|
+
import { ethers } from "ethers"
|
|
4
|
+
import { commons } from "@0xsequence/core"
|
|
5
|
+
import { FeeOption, proto } from "@0xsequence/relayer"
|
|
6
|
+
import { isDeferrable } from "./utils"
|
|
7
|
+
|
|
8
|
+
export type AccountSignerOptions = {
|
|
9
|
+
nonceSpace?: ethers.BigNumberish,
|
|
10
|
+
cantValidateBehavior?: 'ignore' | 'eip6492' | 'throw',
|
|
11
|
+
stubSignatureOverrides?: Map<string, string>,
|
|
12
|
+
selectFee?: (
|
|
13
|
+
txs: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish,
|
|
14
|
+
options: FeeOption[]
|
|
15
|
+
) => Promise<FeeOption | undefined>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function encodeGasRefundTransaction(option?: FeeOption) {
|
|
19
|
+
if (!option) return []
|
|
20
|
+
|
|
21
|
+
const value = ethers.BigNumber.from(option.value)
|
|
22
|
+
|
|
23
|
+
switch (option.token.type) {
|
|
24
|
+
case proto.FeeTokenType.UNKNOWN:
|
|
25
|
+
return [{
|
|
26
|
+
delegateCall: false,
|
|
27
|
+
revertOnError: true,
|
|
28
|
+
gasLimit: option.gasLimit,
|
|
29
|
+
to: option.to,
|
|
30
|
+
value: value.toHexString(),
|
|
31
|
+
data: []
|
|
32
|
+
}]
|
|
33
|
+
|
|
34
|
+
case proto.FeeTokenType.ERC20_TOKEN:
|
|
35
|
+
if (!option.token.contractAddress) {
|
|
36
|
+
throw new Error(`No contract address for ERC-20 fee option`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [{
|
|
40
|
+
delegateCall: false,
|
|
41
|
+
revertOnError: true,
|
|
42
|
+
gasLimit: option.gasLimit,
|
|
43
|
+
to: option.token.contractAddress,
|
|
44
|
+
value: 0,
|
|
45
|
+
data: new ethers.utils.Interface([{
|
|
46
|
+
"constant": false,
|
|
47
|
+
"inputs": [
|
|
48
|
+
{"type": "address"},
|
|
49
|
+
{"type": "uint256"}
|
|
50
|
+
],
|
|
51
|
+
"name": "transfer",
|
|
52
|
+
"outputs": [],
|
|
53
|
+
"type": "function"
|
|
54
|
+
}]).encodeFunctionData('transfer', [option.to, value.toHexString()])
|
|
55
|
+
}]
|
|
56
|
+
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unhandled fee token type ${option.token.type}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class AccountSigner implements ethers.Signer {
|
|
63
|
+
public readonly _isSigner = true
|
|
64
|
+
|
|
65
|
+
constructor (
|
|
66
|
+
public account: Account,
|
|
67
|
+
public chainId: ChainId,
|
|
68
|
+
public readonly options?: AccountSignerOptions
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
get provider() {
|
|
72
|
+
return this.account.providerFor(this.chainId)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getAddress(): Promise<string> {
|
|
76
|
+
return this.account.address
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
signMessage(
|
|
80
|
+
message: string | ethers.utils.Bytes
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
return this.account.signMessage(
|
|
83
|
+
message,
|
|
84
|
+
this.chainId,
|
|
85
|
+
this.options?.cantValidateBehavior ?? 'throw'
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async defaultSelectFee(
|
|
90
|
+
_txs: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish,
|
|
91
|
+
options: FeeOption[]
|
|
92
|
+
): Promise<FeeOption | undefined> {
|
|
93
|
+
// If no options, return undefined
|
|
94
|
+
if (options.length === 0) return undefined
|
|
95
|
+
|
|
96
|
+
// If there are multiple options, try them one by one
|
|
97
|
+
// until we find one that satisfies the balance requirement
|
|
98
|
+
const balanceOfAbi = [{
|
|
99
|
+
"constant": true,
|
|
100
|
+
"inputs": [{"type": "address"}],
|
|
101
|
+
"name": "balanceOf",
|
|
102
|
+
"outputs": [{"type": "uint256"}],
|
|
103
|
+
"type": "function"
|
|
104
|
+
}]
|
|
105
|
+
|
|
106
|
+
for (const option of options) {
|
|
107
|
+
if (option.token.type === proto.FeeTokenType.UNKNOWN) {
|
|
108
|
+
// Native token
|
|
109
|
+
const balance = await this.getBalance()
|
|
110
|
+
if (balance.gte(ethers.BigNumber.from(option.value))) {
|
|
111
|
+
return option
|
|
112
|
+
}
|
|
113
|
+
} else if (option.token.contractAddress && option.token.type === proto.FeeTokenType.ERC20_TOKEN) {
|
|
114
|
+
// ERC20 token
|
|
115
|
+
const token = new ethers.Contract(option.token.contractAddress, balanceOfAbi, this.provider)
|
|
116
|
+
const balance = await token.balanceOf(this.account.address)
|
|
117
|
+
if (balance.gte(ethers.BigNumber.from(option.value))) {
|
|
118
|
+
return option
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// Unsupported token type
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error("No fee option available - not enough balance")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async sendTransaction(
|
|
129
|
+
txsPromise: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | commons.transaction.Transactionish
|
|
130
|
+
): Promise<ethers.providers.TransactionResponse> {
|
|
131
|
+
const txs = isDeferrable(txsPromise) ? (
|
|
132
|
+
await ethers.utils.resolveProperties(txsPromise as ethers.utils.Deferrable<ethers.providers.TransactionRequest>))
|
|
133
|
+
: txsPromise
|
|
134
|
+
|
|
135
|
+
const prepare = await this.account.prepareTransactions({
|
|
136
|
+
txs,
|
|
137
|
+
chainId: this.chainId,
|
|
138
|
+
stubSignatureOverrides: this.options?.stubSignatureOverrides ?? new Map()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const selectMethod = this.options?.selectFee ?? this.defaultSelectFee.bind(this)
|
|
142
|
+
const feeOption = await selectMethod(txs, prepare.feeOptions)
|
|
143
|
+
|
|
144
|
+
const finalTransactions = [
|
|
145
|
+
...prepare.transactions,
|
|
146
|
+
...encodeGasRefundTransaction(feeOption)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return this.account.sendTransaction(
|
|
150
|
+
finalTransactions,
|
|
151
|
+
this.chainId,
|
|
152
|
+
prepare.feeQuote,
|
|
153
|
+
undefined,
|
|
154
|
+
undefined,
|
|
155
|
+
this.options?.nonceSpace ? {
|
|
156
|
+
nonceSpace: this.options.nonceSpace
|
|
157
|
+
} : undefined
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getBalance(blockTag?: ethers.providers.BlockTag | undefined): Promise<ethers.BigNumber> {
|
|
162
|
+
return this.provider.getBalance(this.account.address, blockTag)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
call(
|
|
166
|
+
transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>,
|
|
167
|
+
blockTag?: ethers.providers.BlockTag | undefined
|
|
168
|
+
): Promise<string> {
|
|
169
|
+
return this.provider.call(transaction, blockTag)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async resolveName(name: string): Promise<string> {
|
|
173
|
+
const res = await this.provider.resolveName(name)
|
|
174
|
+
if (!res) throw new Error(`Could not resolve name ${name}`)
|
|
175
|
+
return res
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
connect(_provider: ethers.providers.Provider): ethers.Signer {
|
|
179
|
+
throw new Error("Method not implemented.")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
signTransaction(
|
|
183
|
+
transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>
|
|
184
|
+
): Promise<string> {
|
|
185
|
+
throw new Error("Method not implemented.")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getTransactionCount(blockTag?: ethers.providers.BlockTag | undefined): Promise<number> {
|
|
189
|
+
throw new Error("Method not implemented.")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
estimateGas(
|
|
193
|
+
transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>
|
|
194
|
+
): Promise<ethers.BigNumber> {
|
|
195
|
+
throw new Error("Method not implemented.")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getChainId(): Promise<number> {
|
|
199
|
+
return Promise.resolve(ethers.BigNumber.from(this.chainId).toNumber())
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getGasPrice(): Promise<ethers.BigNumber> {
|
|
203
|
+
throw new Error("Method not implemented.")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getFeeData(): Promise<ethers.providers.FeeData> {
|
|
207
|
+
throw new Error("Method not implemented.")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
checkTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): ethers.utils.Deferrable<ethers.providers.TransactionRequest> {
|
|
211
|
+
throw new Error("Method not implemented.")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
populateTransaction(transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest>): Promise<ethers.providers.TransactionRequest> {
|
|
215
|
+
throw new Error("Method not implemented.")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_checkProvider(operation?: string | undefined): void {
|
|
219
|
+
throw new Error("Method not implemented.")
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ethers } from "ethers"
|
|
2
|
+
|
|
3
|
+
function isPromise(value: any): value is Promise<any> {
|
|
4
|
+
return !!value && typeof value.then === 'function'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isDeferrable<T>(value: any): value is ethers.utils.Deferrable<T> {
|
|
8
|
+
// The value is deferrable if any of the properties is a Promises
|
|
9
|
+
if (typeof(value) === "object") {
|
|
10
|
+
return Object.keys(value).some((key) => isPromise(value[key]))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return false
|
|
14
|
+
}
|