@bitpay-labs/bitcore-wallet-service 11.11.1 → 11.11.3
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/Dockerfile +1 -1
- package/package.json +5 -5
- package/src/lib/chain/btc/index.ts +3 -2
- package/src/lib/chain/eth/index.ts +134 -117
- package/src/lib/chain/sol/index.ts +1 -1
- package/src/lib/chain/xrp/index.ts +1 -1
- package/src/lib/common/constants.ts +1 -1
- package/src/lib/common/utils.ts +18 -1
- package/src/lib/model/tsskeygen.ts +16 -0
- package/src/lib/routes/maintenance.ts +11 -2
- package/src/lib/routes/middleware/authTssRequest.ts +28 -16
- package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
- package/src/lib/routes/services.ts +7 -1
- package/src/lib/routes/tss.ts +33 -30
- package/src/lib/routes/wallets.ts +3 -1
- package/src/lib/server.ts +198 -105
- package/src/lib/storage.ts +129 -20
- package/src/lib/tss.ts +111 -7
- package/test/chain/eth.test.ts +70 -0
- package/test/chain/sol.test.ts +1 -1
- package/test/chain/xrp.test.ts +1 -1
- package/test/expressapp.test.ts +243 -0
- package/test/integration/addressReads.test.ts +503 -0
- package/test/integration/bcmonitor.test.ts +44 -0
- package/test/integration/server.test.ts +251 -49
- package/test/integration/tss.test.ts +428 -0
- package/test/integration/walletReads.test.ts +488 -0
- package/ts_build/package.json +4 -4
- package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +3 -2
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +114 -102
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +1 -1
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +1 -1
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +1 -1
- package/ts_build/src/lib/common/constants.js +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -1
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +1 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +16 -1
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +3 -0
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
- package/ts_build/src/lib/routes/maintenance.js +12 -4
- package/ts_build/src/lib/routes/maintenance.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +6 -1
- package/ts_build/src/lib/routes/services.js.map +1 -1
- package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
- package/ts_build/src/lib/routes/tss.js +32 -30
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
- package/ts_build/src/lib/routes/wallets.js +2 -1
- package/ts_build/src/lib/routes/wallets.js.map +1 -1
- package/ts_build/src/lib/server.d.ts +24 -3
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +148 -97
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +26 -10
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +99 -20
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +12 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +64 -5
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/chain/eth.test.js +59 -0
- package/ts_build/test/chain/eth.test.js.map +1 -1
- package/ts_build/test/chain/sol.test.js +1 -1
- package/ts_build/test/chain/sol.test.js.map +1 -1
- package/ts_build/test/chain/xrp.test.js +1 -1
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/expressapp.test.js +222 -0
- package/ts_build/test/expressapp.test.js.map +1 -1
- package/ts_build/test/integration/addressReads.test.d.ts +2 -0
- package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
- package/ts_build/test/integration/addressReads.test.js +465 -0
- package/ts_build/test/integration/addressReads.test.js.map +1 -0
- package/ts_build/test/integration/bcmonitor.test.js +40 -0
- package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +228 -48
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.d.ts.map +1 -1
- package/ts_build/test/integration/tss.test.js +344 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
- package/ts_build/test/integration/walletReads.test.d.ts +2 -0
- package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
- package/ts_build/test/integration/walletReads.test.js +436 -0
- package/ts_build/test/integration/walletReads.test.js.map +1 -0
package/src/lib/routes/tss.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import * as Types from '../../types/expressapp';
|
|
3
3
|
import { TssKeyGen, TssSign } from '../tss';
|
|
4
|
-
import { authRequest } from './middleware/authRequest';
|
|
5
4
|
import { authTssKeyGenRequest, authTssSignRequest } from './middleware/authTssRequest';
|
|
6
5
|
import { createWalletLimiter } from './middleware/createWalletLimiter';
|
|
7
6
|
import { verifyTssMessage } from './middleware/verifyTssMessage';
|
|
@@ -49,6 +48,36 @@ export class TssRouter {
|
|
|
49
48
|
}
|
|
50
49
|
});
|
|
51
50
|
|
|
51
|
+
// Must be before /keygen/:id/:round so "secret" doesn't get interpreted as a round number
|
|
52
|
+
router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const id = req.params.id;
|
|
55
|
+
const copayerId = req.headers['x-identity'];
|
|
56
|
+
const { chain, network } = req.query as { [key: string]: string };
|
|
57
|
+
const secret = await TssKeyGen.getBwsJoinSecret({ id, chain, network, copayerId });
|
|
58
|
+
return res.json({ secret });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return returnError(err ?? 'unknown', res, req);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
|
|
65
|
+
try {
|
|
66
|
+
const id = req.params.id;
|
|
67
|
+
const { chain, network, secrets, secret } = req.body;
|
|
68
|
+
const copayerId = req.headers['x-identity'];
|
|
69
|
+
if (secrets == null && secret != null) {
|
|
70
|
+
// Legacy (< v1.3) clients send a single unencrypted secret
|
|
71
|
+
await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
|
|
72
|
+
} else {
|
|
73
|
+
await TssKeyGen.storeBwsJoinSecrets({ id, chain, network, secrets, copayerId });
|
|
74
|
+
}
|
|
75
|
+
return res.send();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return returnError(err ?? 'unknown', res, req);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
52
81
|
router.get('/v1/tss/keygen/:id/:round', authTssKeyGenRequest(), async function(req, res) {
|
|
53
82
|
let interval: NodeJS.Timeout;
|
|
54
83
|
try {
|
|
@@ -58,10 +87,6 @@ export class TssRouter {
|
|
|
58
87
|
const { id, round } = req.params as { [key: string]: string };
|
|
59
88
|
const { maxWaitTime } = req.query as { [key: string]: string };
|
|
60
89
|
const copayerId = req.headers['x-identity'];
|
|
61
|
-
if (round === 'secret') {
|
|
62
|
-
const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
|
|
63
|
-
return res.json({ secret });
|
|
64
|
-
}
|
|
65
90
|
|
|
66
91
|
// Validate access and fetch session before committing to a streaming response, so that errors like
|
|
67
92
|
// "session not found" or "not a participant" can still be returned with a proper
|
|
@@ -100,33 +125,10 @@ export class TssRouter {
|
|
|
100
125
|
}
|
|
101
126
|
});
|
|
102
127
|
|
|
103
|
-
router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
|
|
104
|
-
try {
|
|
105
|
-
const id = req.params.id;
|
|
106
|
-
const { secret } = req.body;
|
|
107
|
-
const copayerId = req.headers['x-identity'];
|
|
108
|
-
await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
|
|
109
|
-
return res.send();
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return returnError(err ?? 'unknown', res, req);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
|
|
116
|
-
try {
|
|
117
|
-
const id = req.params.id;
|
|
118
|
-
const copayerId = req.headers['x-identity'];
|
|
119
|
-
const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
|
|
120
|
-
return res.json({ secret });
|
|
121
|
-
} catch (err) {
|
|
122
|
-
return returnError(err ?? 'unknown', res, req);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
128
|
|
|
127
129
|
/** Signature Methods */
|
|
128
130
|
|
|
129
|
-
router.post('/v1/tss/sign/:id',
|
|
131
|
+
router.post('/v1/tss/sign/:id', authTssSignRequest({ joinable: true }), verifyTssMessage, async function(req, res) {
|
|
130
132
|
try {
|
|
131
133
|
const id = req.params.id;
|
|
132
134
|
// version was not given by client until 1.1, so fallback to 1.0
|
|
@@ -178,7 +180,8 @@ export class TssRouter {
|
|
|
178
180
|
try {
|
|
179
181
|
const id = req.params.id;
|
|
180
182
|
const { signature } = req.body;
|
|
181
|
-
|
|
183
|
+
const copayerId = req.headers['x-identity'];
|
|
184
|
+
await TssSign.storeSignature({ id, signature, copayerId });
|
|
182
185
|
return res.send();
|
|
183
186
|
} catch (err) {
|
|
184
187
|
return returnError(err ?? 'unknown', res, req);
|
|
@@ -228,8 +228,10 @@ export function registerWalletRoutes(router: express.Router, context: RouteConte
|
|
|
228
228
|
return res.json([]);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// a copayer that isn't an object with a string id can't exist, so it isn't looked up
|
|
232
|
+
const wellFormed = copayers.filter(c => typeof c?.copayerId === 'string');
|
|
231
233
|
const storage = WalletService.getStorage();
|
|
232
|
-
const existing = await Promise.all<{ copayerId: string; verified: boolean }>(
|
|
234
|
+
const existing = await Promise.all<{ copayerId: string; verified: boolean }>(wellFormed.map(async c => {
|
|
233
235
|
const copayer = await storage.fetchCopayerLookup(c.copayerId).catch(() => null);
|
|
234
236
|
if (!copayer) {
|
|
235
237
|
return { copayerId: c.copayerId, verified: false };
|
package/src/lib/server.ts
CHANGED
|
@@ -604,6 +604,7 @@ export class WalletService implements IWalletService {
|
|
|
604
604
|
* @param {number} [opts.tssVersion] TSS version to use. Supplying this with n > 1 and a multisig chain (e.g. btc) will tell the wallet to use
|
|
605
605
|
* threshold insead of on-chain multisig. Otherwise, n > 1 and a non-multisig chain will default to TSS_KEYGEN_SCHEME_VERSION
|
|
606
606
|
* @param {string} [opts.tssKeyId] TSS key session id. This is the id of the TSS key generation session that will be used to create the wallet.
|
|
607
|
+
* @param {string} [opts.tssKeygenCopayerId] The ID of the copayer creating the wallet. Required for TSS version >= 1.3.
|
|
607
608
|
*/
|
|
608
609
|
async createWallet(opts, cb) {
|
|
609
610
|
let pubKey;
|
|
@@ -674,6 +675,17 @@ export class WalletService implements IWalletService {
|
|
|
674
675
|
return cb(new ClientError('Invalid TSS version'));
|
|
675
676
|
}
|
|
676
677
|
|
|
678
|
+
if (opts.tssVersion >= 1.3) {
|
|
679
|
+
if (!opts.tssKeygenCopayerId) {
|
|
680
|
+
return cb(new ClientError('Copayer ID is required for TSS version >= 1.3'));
|
|
681
|
+
}
|
|
682
|
+
// This is only a sanity check to prevent problems down the line. If this is somehow
|
|
683
|
+
// spoofed, there are more strict authorization checks later in the wallet creation flow.
|
|
684
|
+
if (keySession.participants.indexOf(opts.tssKeygenCopayerId) !== 0) {
|
|
685
|
+
return cb(new ClientError('Only the TSS session leader can create a wallet'));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
677
689
|
// TSS wallets behave like a single-sig
|
|
678
690
|
opts.m = 1;
|
|
679
691
|
opts.n = 1;
|
|
@@ -873,26 +885,29 @@ export class WalletService implements IWalletService {
|
|
|
873
885
|
pendingTxps?: ITxProposal[];
|
|
874
886
|
preferences?: boolean;
|
|
875
887
|
} = {};
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
next => {
|
|
879
|
-
this.getWallet({}, (err, wallet: Partial<Wallet>) => {
|
|
880
|
-
if (err) return next(err);
|
|
881
|
-
if (this._upgradeNeeded(UPGRADES.SOL_bwc_$lt_10_10_12, wallet)) {
|
|
882
|
-
return next(Errors.UPGRADE_NEEDED);
|
|
883
|
-
}
|
|
888
|
+
this.getWallet({}, (err, fetchedWallet: Wallet) => {
|
|
889
|
+
if (err) return cb(err);
|
|
884
890
|
|
|
891
|
+
// Read the wallet once, and give each step its own copy, the same as reading it again: the steps change their
|
|
892
|
+
// wallet in place (this one strips copayer data, the balance code sets tokenAddress on it), and the balance step
|
|
893
|
+
// can store its copy whole.
|
|
894
|
+
const copyOfWallet = () => Wallet.fromObj(fetchedWallet.toObject());
|
|
895
|
+
const omit = (obj, keys: string[]): any => Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key)));
|
|
896
|
+
async.parallel(
|
|
897
|
+
[
|
|
898
|
+
next => {
|
|
899
|
+
let wallet: Partial<Wallet> = copyOfWallet();
|
|
885
900
|
const walletExtendedKeys = ['publicKeyRing', 'pubKey', 'addressManager'];
|
|
886
901
|
const copayerExtendedKeys = ['xPubKey', 'requestPubKey', 'signature', 'addressManager', 'customData'];
|
|
887
902
|
|
|
888
903
|
wallet.copayers = (wallet.copayers || []).map(copayer => {
|
|
889
904
|
if (copayer.id == this.copayerId) return copayer;
|
|
890
|
-
return
|
|
905
|
+
return omit(copayer, ['customData']);
|
|
891
906
|
});
|
|
892
907
|
if (!opts.includeExtendedInfo) {
|
|
893
|
-
wallet =
|
|
908
|
+
wallet = omit(wallet, walletExtendedKeys);
|
|
894
909
|
wallet.copayers = (wallet.copayers || []).map(copayer => {
|
|
895
|
-
return
|
|
910
|
+
return omit(copayer, copayerExtendedKeys);
|
|
896
911
|
}) as Copayer[];
|
|
897
912
|
}
|
|
898
913
|
status.wallet = wallet;
|
|
@@ -903,46 +918,45 @@ export class WalletService implements IWalletService {
|
|
|
903
918
|
status.serverMessage = deprecatedServerMessage(wallet, this.appName, this.appVersion);
|
|
904
919
|
}
|
|
905
920
|
next();
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
921
|
+
},
|
|
922
|
+
next => {
|
|
923
|
+
this.getBalance({ ...opts, wallet: copyOfWallet() }, (err, balance) => {
|
|
924
|
+
// ignore WALLET_NEED_SCAN err is includeExtendedInfo is given
|
|
925
|
+
// (to allow `importWallet` to import a wallet, while scan has
|
|
926
|
+
// failed)
|
|
927
|
+
if (opts.includeExtendedInfo) {
|
|
928
|
+
if (err && err.code != 'WALLET_NEED_SCAN') {
|
|
929
|
+
return next(err);
|
|
930
|
+
}
|
|
931
|
+
} else if (err) {
|
|
916
932
|
return next(err);
|
|
917
933
|
}
|
|
918
|
-
} else if (err) {
|
|
919
|
-
return next(err);
|
|
920
|
-
}
|
|
921
934
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
935
|
+
status.balance = balance;
|
|
936
|
+
next();
|
|
937
|
+
});
|
|
938
|
+
},
|
|
939
|
+
next => {
|
|
940
|
+
this.getPendingTxs({ ...opts, wallet: copyOfWallet() }, (err, pendingTxps) => {
|
|
941
|
+
if (err) return next(err);
|
|
942
|
+
status.pendingTxps = pendingTxps;
|
|
943
|
+
next();
|
|
944
|
+
});
|
|
945
|
+
},
|
|
946
|
+
next => {
|
|
947
|
+
this.getPreferences({}, (err, preferences) => {
|
|
948
|
+
if (err) return next(err);
|
|
949
|
+
status.preferences = preferences;
|
|
950
|
+
next();
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
],
|
|
954
|
+
err => {
|
|
955
|
+
if (err) return cb(err);
|
|
956
|
+
return cb(null, status);
|
|
939
957
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (err) return cb(err);
|
|
943
|
-
return cb(null, status);
|
|
944
|
-
}
|
|
945
|
-
);
|
|
958
|
+
);
|
|
959
|
+
});
|
|
946
960
|
}
|
|
947
961
|
|
|
948
962
|
/**
|
|
@@ -1036,7 +1050,11 @@ export class WalletService implements IWalletService {
|
|
|
1036
1050
|
this._notify(type, data, {}, cb);
|
|
1037
1051
|
}
|
|
1038
1052
|
|
|
1039
|
-
_addCopayerToWallet(
|
|
1053
|
+
_addCopayerToWallet(
|
|
1054
|
+
wallet: Wallet,
|
|
1055
|
+
opts: JoinWalletOpts,
|
|
1056
|
+
cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
|
|
1057
|
+
) {
|
|
1040
1058
|
const copayer = Copayer.create({
|
|
1041
1059
|
coin: wallet.coin,
|
|
1042
1060
|
chain: wallet.chain, // chain === coin for stored clients
|
|
@@ -1192,20 +1210,11 @@ export class WalletService implements IWalletService {
|
|
|
1192
1210
|
|
|
1193
1211
|
/**
|
|
1194
1212
|
* Joins a wallet in creation.
|
|
1195
|
-
* @param {Object} opts
|
|
1196
|
-
* @param {string} opts.walletId The wallet id.
|
|
1197
|
-
* @param {string} opts.coin The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc
|
|
1198
|
-
* @param {string} opts.chain The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc
|
|
1199
|
-
* @param {string} opts.name The copayer name.
|
|
1200
|
-
* @param {string} opts.xPubKey Extended Public Key for this copayer
|
|
1201
|
-
* @param {string} opts.hardwareSourcePublicKey Public key from a hardware device for this copayer
|
|
1202
|
-
* @param {string} opts.clientDerivedPublicKey Public key from the client for this wallet
|
|
1203
|
-
* @param {string} opts.requestPubKey Public Key used to check requests from this copayer.
|
|
1204
|
-
* @param {string} opts.copayerSignature S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
|
|
1205
|
-
* @param {string} [opts.customData] Custom data for this copayer.
|
|
1206
|
-
* @param {boolean} [opts.dryRun] Simulate the action but do not change server state.
|
|
1207
1213
|
*/
|
|
1208
|
-
joinWallet(
|
|
1214
|
+
joinWallet(
|
|
1215
|
+
opts: JoinWalletOpts,
|
|
1216
|
+
cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
|
|
1217
|
+
) {
|
|
1209
1218
|
// xPubKey is the canonical copayer identity in the current protocol. Alternate public-key
|
|
1210
1219
|
// fields are ancillary metadata; supporting an xPubKey-less copayer requires a versioned
|
|
1211
1220
|
// end-to-end identity protocol rather than a local identity fallback.
|
|
@@ -1224,6 +1233,9 @@ export class WalletService implements IWalletService {
|
|
|
1224
1233
|
opts.chain = opts.coin; // chain === coin for stored clients
|
|
1225
1234
|
}
|
|
1226
1235
|
if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new ClientError('Invalid coin'));
|
|
1236
|
+
if (opts.tssKeygenChain != null && !Utils.checkValueInCollection(opts.tssKeygenChain, Constants.CHAINS)) {
|
|
1237
|
+
return cb(new ClientError('Invalid TSS keygen chain'));
|
|
1238
|
+
}
|
|
1227
1239
|
|
|
1228
1240
|
// xPubKey is parsed and validated for every join; ancillary fields never suppress it.
|
|
1229
1241
|
// A future update will re-add conditional logic which allows opts.hardwareSourcePublicKey
|
|
@@ -1284,8 +1296,16 @@ export class WalletService implements IWalletService {
|
|
|
1284
1296
|
|
|
1285
1297
|
if (wallet.tssKeyId) {
|
|
1286
1298
|
const keySession = await storage.fetchTssKeyGenSession({ id: wallet.tssKeyId });
|
|
1287
|
-
|
|
1288
|
-
|
|
1299
|
+
// A sibling wallet on another network (e.g. livenet -> testnet) has an xPubKey serialized for that network,
|
|
1300
|
+
// but the keygen session's copayer IDs were derived from the xPubKey serialized for the session's network.
|
|
1301
|
+
const idChain = opts.tssKeygenChain || opts.chain;
|
|
1302
|
+
const copayerIds = new Set([
|
|
1303
|
+
Copayer.xPubToCopayerId(idChain, opts.xPubKey), // this is probably a duplicate of one of the next two entries, but just in case.
|
|
1304
|
+
Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'livenet')),
|
|
1305
|
+
Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'testnet'))
|
|
1306
|
+
]);
|
|
1307
|
+
|
|
1308
|
+
if (!keySession?.participants.some(id => copayerIds.has(id))) {
|
|
1289
1309
|
return cb(Errors.TSS_NON_PARTICIPANT);
|
|
1290
1310
|
}
|
|
1291
1311
|
return this._addCopayerToWallet(wallet, opts, cb);
|
|
@@ -1535,9 +1555,10 @@ export class WalletService implements IWalletService {
|
|
|
1535
1555
|
_canCreateAddress(ignoreMaxGap, cb) {
|
|
1536
1556
|
if (ignoreMaxGap) return cb(null, true);
|
|
1537
1557
|
|
|
1538
|
-
|
|
1558
|
+
const newest = { noChange: true, reverse: true, limit: Defaults.MAX_MAIN_ADDRESS_GAP };
|
|
1559
|
+
this.storage.fetchAddressesPage(this.walletId, newest, (err, addresses: IAddress[]) => {
|
|
1539
1560
|
if (err) return cb(err);
|
|
1540
|
-
const latestAddresses = addresses.
|
|
1561
|
+
const latestAddresses = addresses.reverse();
|
|
1541
1562
|
if (
|
|
1542
1563
|
latestAddresses.length < Defaults.MAX_MAIN_ADDRESS_GAP ||
|
|
1543
1564
|
latestAddresses.some(a => a.hasActivity)
|
|
@@ -1638,7 +1659,7 @@ export class WalletService implements IWalletService {
|
|
|
1638
1659
|
};
|
|
1639
1660
|
|
|
1640
1661
|
const getFirstAddress = (wallet, cb) => {
|
|
1641
|
-
this.storage.
|
|
1662
|
+
this.storage.fetchAddressesPage(this.walletId, { limit: 1 }, (err, addresses) => {
|
|
1642
1663
|
if (err) return cb(err);
|
|
1643
1664
|
if (addresses?.length) {
|
|
1644
1665
|
const x = addresses[0];
|
|
@@ -1691,17 +1712,9 @@ export class WalletService implements IWalletService {
|
|
|
1691
1712
|
|
|
1692
1713
|
getAddresses(opts: GetAddressesOpts, cb) {
|
|
1693
1714
|
opts = opts || {};
|
|
1694
|
-
|
|
1715
|
+
const { noChange, addresses: only, reverse, skip, limit } = opts;
|
|
1716
|
+
this.storage.fetchAddressesPage(this.walletId, { noChange, addresses: only, reverse, skip, limit }, (err, addresses) => {
|
|
1695
1717
|
if (err) return cb(err);
|
|
1696
|
-
if (opts.noChange) {
|
|
1697
|
-
addresses = addresses.filter(a => !a.isChange);
|
|
1698
|
-
}
|
|
1699
|
-
if (opts.addresses) {
|
|
1700
|
-
addresses = addresses.filter(a => opts.addresses.includes(a.address));
|
|
1701
|
-
}
|
|
1702
|
-
if (opts.reverse) addresses.reverse();
|
|
1703
|
-
if (opts.skip > 0) addresses = addresses.slice(opts.skip);
|
|
1704
|
-
if (opts.limit > 0) addresses = addresses.slice(0, opts.limit);
|
|
1705
1718
|
|
|
1706
1719
|
this.getWallet({}, (err, wallet) => {
|
|
1707
1720
|
if (err) return cb(err);
|
|
@@ -1783,7 +1796,7 @@ export class WalletService implements IWalletService {
|
|
|
1783
1796
|
async.series(
|
|
1784
1797
|
[
|
|
1785
1798
|
next => {
|
|
1786
|
-
|
|
1799
|
+
const useWallet = (err, w) => {
|
|
1787
1800
|
if (err) return next(err);
|
|
1788
1801
|
|
|
1789
1802
|
wallet = w;
|
|
@@ -1793,7 +1806,10 @@ export class WalletService implements IWalletService {
|
|
|
1793
1806
|
bc = this._getBlockchainExplorer(wallet.chain, wallet.network);
|
|
1794
1807
|
if (!bc) return cb(new Error('Could not get blockchain explorer instance'));
|
|
1795
1808
|
return next();
|
|
1796
|
-
}
|
|
1809
|
+
};
|
|
1810
|
+
// the balance code passes the wallet it already has
|
|
1811
|
+
if (opts.wallet) return useWallet(null, opts.wallet);
|
|
1812
|
+
this.getWallet({}, useWallet);
|
|
1797
1813
|
},
|
|
1798
1814
|
next => {
|
|
1799
1815
|
if (Array.isArray(opts.addresses)) {
|
|
@@ -1889,7 +1905,7 @@ export class WalletService implements IWalletService {
|
|
|
1889
1905
|
next => {
|
|
1890
1906
|
utxoIndex = _.keyBy(allUtxos, utxoKey);
|
|
1891
1907
|
|
|
1892
|
-
this.getPendingTxs({ numberFormat: opts.numberFormat }, (err, txps) => {
|
|
1908
|
+
this.getPendingTxs({ numberFormat: opts.numberFormat, wallet }, (err, txps) => {
|
|
1893
1909
|
if (err) return next(err);
|
|
1894
1910
|
|
|
1895
1911
|
const lockedInputs = txps.flatMap(t => t.inputs).map(utxoKey);
|
|
@@ -1933,7 +1949,8 @@ export class WalletService implements IWalletService {
|
|
|
1933
1949
|
});
|
|
1934
1950
|
logger.debug(`Got ${allUtxos.length} usable UTXOs`);
|
|
1935
1951
|
return next();
|
|
1936
|
-
}
|
|
1952
|
+
},
|
|
1953
|
+
wallet
|
|
1937
1954
|
);
|
|
1938
1955
|
},
|
|
1939
1956
|
next => {
|
|
@@ -1946,6 +1963,7 @@ export class WalletService implements IWalletService {
|
|
|
1946
1963
|
}
|
|
1947
1964
|
utxo.path = addressToPath[utxo.address].path;
|
|
1948
1965
|
utxo.publicKeys = addressToPath[utxo.address].publicKeys;
|
|
1966
|
+
utxo.isChange = addressToPath[utxo.address].isChange;
|
|
1949
1967
|
}
|
|
1950
1968
|
return next();
|
|
1951
1969
|
}
|
|
@@ -2399,6 +2417,37 @@ export class WalletService implements IWalletService {
|
|
|
2399
2417
|
}
|
|
2400
2418
|
|
|
2401
2419
|
_validateAndSanitizeTxOpts(wallet, opts, cb) {
|
|
2420
|
+
const gasFeeFields = ['gasPrice', 'maxGasFee', 'priorityGasFee'];
|
|
2421
|
+
const hasGasFeeOverride = gasFeeFields.some(key => opts[key] != null);
|
|
2422
|
+
if (hasGasFeeOverride) {
|
|
2423
|
+
if (!Constants.EVM_CHAINS[wallet.chain.toUpperCase()]) {
|
|
2424
|
+
return cb(new ClientError('Gas fee overrides are only supported for EVM chains'));
|
|
2425
|
+
}
|
|
2426
|
+
for (const key of gasFeeFields) {
|
|
2427
|
+
if (opts[key] != null && (!Number.isSafeInteger(opts[key]) || opts[key] <= 0)) {
|
|
2428
|
+
return cb(new ClientError(`${key} must be a positive safe integer in wei`));
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
if (opts.gasPrice != null && Number(opts.txType || 0) !== 0) {
|
|
2432
|
+
return cb(new ClientError('gasPrice requires a type 0 transaction'));
|
|
2433
|
+
}
|
|
2434
|
+
if ((opts.maxGasFee != null || opts.priorityGasFee != null) && Number(opts.txType) !== 2) {
|
|
2435
|
+
return cb(new ClientError('maxGasFee and priorityGasFee require a type 2 transaction'));
|
|
2436
|
+
}
|
|
2437
|
+
if ((opts.maxGasFee != null) !== (opts.priorityGasFee != null)) {
|
|
2438
|
+
return cb(new ClientError('maxGasFee and priorityGasFee must be supplied together'));
|
|
2439
|
+
}
|
|
2440
|
+
if (opts.priorityGasFee > opts.maxGasFee) {
|
|
2441
|
+
return cb(new ClientError('priorityGasFee must not exceed maxGasFee'));
|
|
2442
|
+
}
|
|
2443
|
+
if (opts.feePerKb != null || opts.fee != null) {
|
|
2444
|
+
return cb(new ClientError('Gas fee overrides cannot be combined with feePerKb or fee'));
|
|
2445
|
+
}
|
|
2446
|
+
// sendMax calculates its amount before getFee, using a separate fee estimate.
|
|
2447
|
+
if (opts.sendMax) {
|
|
2448
|
+
return cb(new ClientError('Gas fee overrides are not supported with sendMax'));
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2402
2451
|
async.series(
|
|
2403
2452
|
[
|
|
2404
2453
|
next => {
|
|
@@ -2406,7 +2455,7 @@ export class WalletService implements IWalletService {
|
|
|
2406
2455
|
boolToNum(!!opts.feeLevel) + boolToNum(Utils.isNumber(opts.feePerKb)) + boolToNum(Utils.isNumber(opts.fee));
|
|
2407
2456
|
if (feeArgs > 1) return next(new ClientError('Only one of feeLevel/feePerKb/fee can be specified'));
|
|
2408
2457
|
|
|
2409
|
-
if (feeArgs == 0) {
|
|
2458
|
+
if (feeArgs == 0 && !hasGasFeeOverride) {
|
|
2410
2459
|
opts.feeLevel = 'normal';
|
|
2411
2460
|
}
|
|
2412
2461
|
|
|
@@ -2418,6 +2467,11 @@ export class WalletService implements IWalletService {
|
|
|
2418
2467
|
);
|
|
2419
2468
|
}
|
|
2420
2469
|
|
|
2470
|
+
// An explicit gas fee determines the price, not the named fee level.
|
|
2471
|
+
if (hasGasFeeOverride) {
|
|
2472
|
+
delete opts.feeLevel;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2421
2475
|
const error = ChainService.checkUtxos(wallet.chain, opts);
|
|
2422
2476
|
if (error) {
|
|
2423
2477
|
return next(new ClientError('fee can only be set when inputs are specified'));
|
|
@@ -2779,6 +2833,9 @@ export class WalletService implements IWalletService {
|
|
|
2779
2833
|
* @param {Boolean} opts.enableRBF - Optional. enable BTC Replace By Fee
|
|
2780
2834
|
* @param {Boolean} opts.replaceTxByFee - Optional. Ignore locked utxos check ( used for replacing a transaction designated as RBF)
|
|
2781
2835
|
* @param {number} opts.txType - Optional. Type of EVM transaction
|
|
2836
|
+
* @param {number} opts.gasPrice - Optional. Type 0 gas price in wei. Overrides feeLevel; incompatible with fee/feePerKb/sendMax.
|
|
2837
|
+
* @param {number} opts.maxGasFee - Optional. Type 2 maximum fee per gas in wei; requires priorityGasFee.
|
|
2838
|
+
* @param {number} opts.priorityGasFee - Optional. Positive type 2 priority fee per gas in wei; requires maxGasFee and must not exceed it. Gas fee overrides cannot be used with fee/feePerKb/sendMax.
|
|
2782
2839
|
* @param {number} opts.gasLimitBuffer - Optional. Percentage of buffer to add to the gasLimit
|
|
2783
2840
|
* @param {number} opts.priorityFeePercentile - Optional. Percentile of targeted priority fee rate
|
|
2784
2841
|
* @param {Boolean} opts.multiTx - Optional. Proposal will create multiple transactions
|
|
@@ -3169,19 +3226,23 @@ export class WalletService implements IWalletService {
|
|
|
3169
3226
|
* @returns {Object} txProposal
|
|
3170
3227
|
*/
|
|
3171
3228
|
getTx(opts, cb: (err: any, txp?: TxProposal) => void) {
|
|
3172
|
-
|
|
3229
|
+
// the tx and its note are both completed from the wallet, so read it once
|
|
3230
|
+
this.storage.fetchWallet(this.walletId, (err, wallet) => {
|
|
3173
3231
|
if (err) return cb(err);
|
|
3174
|
-
|
|
3232
|
+
this.storage.fetchTx(this.walletId, opts.txProposalId, (err, txp) => {
|
|
3233
|
+
if (err) return cb(err);
|
|
3234
|
+
if (!txp) return cb(Errors.TX_NOT_FOUND);
|
|
3175
3235
|
|
|
3176
|
-
|
|
3236
|
+
if (!txp.txid) return cb(null, txp);
|
|
3177
3237
|
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3238
|
+
this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
|
|
3239
|
+
if (err) {
|
|
3240
|
+
this.logw(`Error fetching tx note for ${txp.txid}:`, err);
|
|
3241
|
+
}
|
|
3242
|
+
txp.note = note;
|
|
3243
|
+
return cb(null, txp);
|
|
3244
|
+
}, wallet);
|
|
3245
|
+
}, wallet);
|
|
3185
3246
|
});
|
|
3186
3247
|
}
|
|
3187
3248
|
|
|
@@ -3199,19 +3260,22 @@ export class WalletService implements IWalletService {
|
|
|
3199
3260
|
// foreign wallet's proposal data to any authenticated copayer. The
|
|
3200
3261
|
// internal global consumers (BlockchainMonitor, getWalletFromIdentifier)
|
|
3201
3262
|
// are unaffected; they keep calling storage.fetchTxByHash directly.
|
|
3202
|
-
this.storage.
|
|
3263
|
+
this.storage.fetchWallet(this.walletId, (err, wallet) => {
|
|
3203
3264
|
if (err) return cb(err);
|
|
3204
|
-
|
|
3265
|
+
this.storage.fetchTxByHashForWallet(this.walletId, opts.txid, (err, txp) => {
|
|
3266
|
+
if (err) return cb(err);
|
|
3267
|
+
if (!txp) return cb(Errors.TX_NOT_FOUND);
|
|
3205
3268
|
|
|
3206
|
-
|
|
3269
|
+
if (!txp.txid) return cb(null, txp);
|
|
3207
3270
|
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3271
|
+
this.storage.fetchTxNote(this.walletId, txp.txid, (err, note) => {
|
|
3272
|
+
if (err) {
|
|
3273
|
+
this.logw(`Error fetching tx note for ${txp.txid}:`, err);
|
|
3274
|
+
}
|
|
3275
|
+
txp.note = note;
|
|
3276
|
+
return cb(null, txp);
|
|
3277
|
+
}, wallet);
|
|
3278
|
+
}, wallet);
|
|
3215
3279
|
});
|
|
3216
3280
|
}
|
|
3217
3281
|
|
|
@@ -3841,7 +3905,7 @@ export class WalletService implements IWalletService {
|
|
|
3841
3905
|
return cb(err, txps);
|
|
3842
3906
|
}
|
|
3843
3907
|
);
|
|
3844
|
-
});
|
|
3908
|
+
}, opts.wallet);
|
|
3845
3909
|
}
|
|
3846
3910
|
}
|
|
3847
3911
|
|
|
@@ -4131,7 +4195,7 @@ export class WalletService implements IWalletService {
|
|
|
4131
4195
|
return cb(err);
|
|
4132
4196
|
}
|
|
4133
4197
|
wallet.beRegistered = true;
|
|
4134
|
-
return this.storage.
|
|
4198
|
+
return this.storage.storeWalletRegistration(wallet, err => cb(err, true));
|
|
4135
4199
|
});
|
|
4136
4200
|
}
|
|
4137
4201
|
|
|
@@ -4867,7 +4931,8 @@ export class WalletService implements IWalletService {
|
|
|
4867
4931
|
minTs,
|
|
4868
4932
|
maxTs
|
|
4869
4933
|
},
|
|
4870
|
-
done
|
|
4934
|
+
done,
|
|
4935
|
+
wallet
|
|
4871
4936
|
);
|
|
4872
4937
|
},
|
|
4873
4938
|
done => {
|
|
@@ -4876,7 +4941,8 @@ export class WalletService implements IWalletService {
|
|
|
4876
4941
|
{
|
|
4877
4942
|
minTs
|
|
4878
4943
|
},
|
|
4879
|
-
done
|
|
4944
|
+
done,
|
|
4945
|
+
wallet
|
|
4880
4946
|
);
|
|
4881
4947
|
}
|
|
4882
4948
|
],
|
|
@@ -5790,3 +5856,30 @@ export const UPGRADES = {
|
|
|
5790
5856
|
} as const;
|
|
5791
5857
|
|
|
5792
5858
|
type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
|
|
5859
|
+
|
|
5860
|
+
export interface JoinWalletOpts {
|
|
5861
|
+
/** The wallet id. */
|
|
5862
|
+
walletId: string;
|
|
5863
|
+
/** The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc */
|
|
5864
|
+
coin?: string;
|
|
5865
|
+
/** The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc */
|
|
5866
|
+
chain?: string;
|
|
5867
|
+
/** The copayer name. */
|
|
5868
|
+
name: string;
|
|
5869
|
+
/** Extended Public Key for this copayer */
|
|
5870
|
+
xPubKey: string;
|
|
5871
|
+
/** Public key from a hardware device for this copayer */
|
|
5872
|
+
hardwareSourcePublicKey?: string;
|
|
5873
|
+
/** Public key from the client for this wallet */
|
|
5874
|
+
clientDerivedPublicKey?: string;
|
|
5875
|
+
/** Public Key used to check requests from this copayer. */
|
|
5876
|
+
requestPubKey: string;
|
|
5877
|
+
/** Signature proving knowledge of the wallet secret for this copayer. */
|
|
5878
|
+
copayerSignature: string;
|
|
5879
|
+
/** Custom data for this copayer. */
|
|
5880
|
+
customData?: string;
|
|
5881
|
+
/** Simulate the action but do not change server state. */
|
|
5882
|
+
dryRun?: boolean;
|
|
5883
|
+
/** The chain used for TSS key generation. */
|
|
5884
|
+
tssKeygenChain?: string;
|
|
5885
|
+
};
|