@stellar/typescript-wallet-sdk 5.0.0 → 5.1.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/CHANGELOG.MD +10 -0
- package/lib/bundle.js +160 -7
- package/lib/bundle.js.map +1 -1
- package/lib/bundle_browser.js +9188 -2996
- package/lib/bundle_browser.js.map +1 -1
- package/lib/walletSdk/Exceptions/index.d.ts +12 -0
- package/lib/walletSdk/Recovery/index.d.ts +35 -1
- package/lib/walletSdk/Types/recovery.d.ts +51 -0
- package/package.json +1 -1
- package/src/walletSdk/Exceptions/index.ts +51 -0
- package/src/walletSdk/Recovery/index.ts +120 -4
- package/src/walletSdk/Types/recovery.ts +51 -0
- package/test/customer.test.ts +2 -2
- package/test/recovery.test.ts +258 -22
- package/test/sep38.test.ts +1 -1
- package/test/sep6.test.ts +2 -2
- package/test/stellar.test.ts +13 -13
- package/test/transaction.test.ts +4 -4
- package/test/wallet.test.ts +1 -1
|
@@ -100,6 +100,18 @@ export declare class NoAccountSignersError extends Error {
|
|
|
100
100
|
export declare class DeviceKeyEqualsMasterKeyError extends Error {
|
|
101
101
|
constructor();
|
|
102
102
|
}
|
|
103
|
+
export declare class DuplicateRecoverySignerError extends Error {
|
|
104
|
+
constructor(serverKeys: string[], signerKey: string);
|
|
105
|
+
}
|
|
106
|
+
export declare class RecoverySignerEqualsDeviceKeyError extends Error {
|
|
107
|
+
constructor(serverKey: string, signerKey: string);
|
|
108
|
+
}
|
|
109
|
+
export declare class DuplicateAccountSignerError extends Error {
|
|
110
|
+
constructor(address: string);
|
|
111
|
+
}
|
|
112
|
+
export declare class SignerKeyEqualsMasterKeyError extends Error {
|
|
113
|
+
constructor(address: string);
|
|
114
|
+
}
|
|
103
115
|
export declare class NoAccountAndNoSponsorError extends Error {
|
|
104
116
|
constructor();
|
|
105
117
|
}
|
|
@@ -40,8 +40,21 @@ export declare class Recovery extends AccountRecover {
|
|
|
40
40
|
* **Warning**: This transaction will lock master key of the account. Make sure you have access to
|
|
41
41
|
* specified [RecoverableWalletConfig.deviceAddress]
|
|
42
42
|
*
|
|
43
|
+
* The returned transaction is unsigned — sign it and submit it yourself. It sets signers and
|
|
44
|
+
* thresholds, so it needs signatures meeting the account's current **high** threshold rather
|
|
45
|
+
* than the master key specifically, and the sponsor must sign too when
|
|
46
|
+
* [RecoverableWalletConfig.sponsorAddress] is set. See [RecoverableWallet] for the cases.
|
|
47
|
+
*
|
|
48
|
+
* The signer set returned by the recovery servers is validated before the transaction is built,
|
|
49
|
+
* because undoing the master key lock afterwards takes a further `SetOptions` meeting the
|
|
50
|
+
* account's high threshold, which the collapsed signer set may no longer be able to reach.
|
|
51
|
+
*
|
|
43
52
|
* This transaction can be sponsored.
|
|
44
53
|
* @param {RecoverableWalletConfig} config - The configuration for recoverable wallet.
|
|
54
|
+
* @throws {DeviceKeyEqualsMasterKeyError} If the device address is the account address.
|
|
55
|
+
* @throws {DuplicateRecoverySignerError} If two or more recovery servers return the same signer key.
|
|
56
|
+
* @throws {RecoverySignerEqualsDeviceKeyError} If a recovery server returns the device key.
|
|
57
|
+
* @throws {SignerKeyEqualsMasterKeyError} If a recovery server returns the account's own key.
|
|
45
58
|
* @returns {Promise<RecoverableWallet>} The wallet.
|
|
46
59
|
*/
|
|
47
60
|
createRecoverableWallet(config: RecoverableWalletConfig): Promise<RecoverableWallet>;
|
|
@@ -63,6 +76,9 @@ export declare class Recovery extends AccountRecover {
|
|
|
63
76
|
* @param {AccountThreshold} accountThreshold - Low, medium, and high thresholds to set on the account.
|
|
64
77
|
* @param {AccountKeypair} [sponsorAddress] - Stellar address of the account sponsoring this transaction.
|
|
65
78
|
* @param {(builder: CommonBuilder) => CommonBuilder} [builderExtra] - Stellar address of the account sponsoring this transaction.
|
|
79
|
+
* @throws {DuplicateAccountSignerError} If the same address appears more than once in [accountSigners].
|
|
80
|
+
* @throws {SignerKeyEqualsMasterKeyError} If [accountSigners] contains [account] itself.
|
|
81
|
+
* @throws {NoAccountAndNoSponsorError} If the account does not exist and no sponsor is given.
|
|
66
82
|
* @returns {Promise<Transaction>} The built transaction.
|
|
67
83
|
*/
|
|
68
84
|
registerRecoveryServerSigners(account: AccountKeypair, accountSigners: AccountSigner[], accountThreshold: AccountThreshold, sponsorAddress?: AccountKeypair, builderExtra?: (builder: CommonBuilder) => CommonBuilder): Promise<Transaction>;
|
|
@@ -71,9 +87,27 @@ export declare class Recovery extends AccountRecover {
|
|
|
71
87
|
* [SEP-30](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md).
|
|
72
88
|
* @param {AccountKeypair} account - Account being registerd.
|
|
73
89
|
* @param {RecoveryIdentityMap} identityMap - map of identities to recovery keys.
|
|
74
|
-
* @returns {Promise<
|
|
90
|
+
* @returns {Promise<EnrolledRecoverySigner[]>} Each recovery signer public key paired with the
|
|
91
|
+
* server that returned it. The pairing is kept so that cross-server validation can name the
|
|
92
|
+
* server responsible when two of them return the same key.
|
|
75
93
|
*/
|
|
76
94
|
private enrollWithRecoveryServer;
|
|
95
|
+
/**
|
|
96
|
+
* Assert that the recovery servers collectively returned a usable signer set.
|
|
97
|
+
*
|
|
98
|
+
* SEP-30 has each server generate its own unique signing key for the account,
|
|
99
|
+
* so the same key coming back from two servers is a protocol violation, not a
|
|
100
|
+
* judgement call. Left unchecked it halves the recovery signer set while the
|
|
101
|
+
* thresholds sized for the full set are written anyway, which is only
|
|
102
|
+
* discovered when the device is lost and recovery is attempted.
|
|
103
|
+
* @private
|
|
104
|
+
* @param {EnrolledRecoverySigner[]} enrolled - Signer keys paired with the servers that returned them.
|
|
105
|
+
* @param {string} deviceAddress - Address being added as the device signer.
|
|
106
|
+
* @throws {DuplicateRecoverySignerError} If two or more servers returned the same key.
|
|
107
|
+
* @throws {RecoverySignerEqualsDeviceKeyError} If a server returned the device key.
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*/
|
|
110
|
+
private validateEnrolledSigners;
|
|
77
111
|
private getLatestRecoverySigner;
|
|
78
112
|
private register;
|
|
79
113
|
}
|
|
@@ -22,6 +22,33 @@ export type RecoverableWalletConfig = {
|
|
|
22
22
|
sponsorAddress?: AccountKeypair;
|
|
23
23
|
builderExtra?: (builder: CommonBuilder) => CommonBuilder;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* The result of registering an account with SEP-30 recovery servers.
|
|
27
|
+
* @property {Transaction} transaction - The **unsigned** registration transaction. The SDK builds
|
|
28
|
+
* it but neither signs nor submits it.
|
|
29
|
+
*
|
|
30
|
+
* It sets signers and thresholds, which Stellar authorizes against the account's **high**
|
|
31
|
+
* threshold — not against the master key specifically:
|
|
32
|
+
*
|
|
33
|
+
* - For an account that already exists, whichever of its current signers together reach `high`
|
|
34
|
+
* can authorize it. The master key is neither automatically sufficient (it may carry less
|
|
35
|
+
* weight than `high`) nor necessarily required (an account whose master key is already locked
|
|
36
|
+
* is enrolled by its other signers alone).
|
|
37
|
+
* - For an account created within this same transaction, i.e. the sponsored path where the
|
|
38
|
+
* account does not exist yet, the new account's own key suffices: a new account starts with
|
|
39
|
+
* master weight 1 and zeroed thresholds.
|
|
40
|
+
* - When `sponsorAddress` is supplied the sponsor must sign as well, being the source of the
|
|
41
|
+
* `beginSponsoringFutureReserves` operation and of `createAccount`.
|
|
42
|
+
*
|
|
43
|
+
* For a newly funded account whose master key is still its only signer, that is just
|
|
44
|
+
* `transaction.sign(accountKp.keypair)` followed by `stellar.submitTransaction(transaction)`.
|
|
45
|
+
*
|
|
46
|
+
* Once submitted the master key is locked, so inspect the transaction before signing if you want
|
|
47
|
+
* to verify the signer set it installs.
|
|
48
|
+
* @property {string[]} signers - Public keys of the recovery server signers added to the account,
|
|
49
|
+
* one per configured server. These are the addresses to pass to `signWithRecoveryServers` when
|
|
50
|
+
* recovering the account later.
|
|
51
|
+
*/
|
|
25
52
|
export type RecoverableWallet = {
|
|
26
53
|
transaction: Transaction;
|
|
27
54
|
signers: string[];
|
|
@@ -41,6 +68,30 @@ export type AccountThreshold = {
|
|
|
41
68
|
medium: number;
|
|
42
69
|
high: number;
|
|
43
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Weights assigned to the device signer and to each recovery server signer.
|
|
73
|
+
*
|
|
74
|
+
* These values only mean something relative to {@link AccountThreshold}. Together they decide
|
|
75
|
+
* three separate properties, and it is possible to satisfy some and silently lose the others:
|
|
76
|
+
*
|
|
77
|
+
* 1. The device can operate the account on its own — `device >= high`.
|
|
78
|
+
* 2. The recovery servers can recover the account without the device —
|
|
79
|
+
* `recoveryServer * serverCount >= high`.
|
|
80
|
+
* 3. No single recovery server can act alone — `recoveryServer < low`. This is a choice, not a
|
|
81
|
+
* protocol guarantee: SEP-30 supports both postures. Two or more servers weighted this way
|
|
82
|
+
* means no individual server controls the account, but the spec equally supports a
|
|
83
|
+
* single-server custodial setup where one deliberately does.
|
|
84
|
+
*
|
|
85
|
+
* With two servers, `{ device: 10, recoveryServer: 5 }` against thresholds `{ low: 10, medium: 10,
|
|
86
|
+
* high: 10 }` satisfies all three: the device alone reaches `high`, the two servers together reach
|
|
87
|
+
* `high`, and one server alone reaches nothing.
|
|
88
|
+
*
|
|
89
|
+
* Note that whether the device can act alone is decided by `device` against `high`, per property 1
|
|
90
|
+
* above — not by how `device` compares to `recoveryServer`. A device weighing less than a single
|
|
91
|
+
* recovery server still operates the account independently as long as it meets `high`.
|
|
92
|
+
* @property {number} device - Weight of the device signer, which replaces the master key.
|
|
93
|
+
* @property {number} recoveryServer - Weight given to each recovery server signer.
|
|
94
|
+
*/
|
|
44
95
|
export type SignerWeight = {
|
|
45
96
|
device: number;
|
|
46
97
|
recoveryServer: number;
|
package/package.json
CHANGED
|
@@ -274,6 +274,57 @@ export class DeviceKeyEqualsMasterKeyError extends Error {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
export class DuplicateRecoverySignerError extends Error {
|
|
278
|
+
constructor(serverKeys: string[], signerKey: string) {
|
|
279
|
+
super(
|
|
280
|
+
`Recovery servers [${serverKeys.join(
|
|
281
|
+
", ",
|
|
282
|
+
)}] all returned the signer key ${signerKey}. SEP-30 requires each ` +
|
|
283
|
+
`server to generate a unique signing key, so this is either one ` +
|
|
284
|
+
`endpoint configured under several keys in the recovery server map, ` +
|
|
285
|
+
`or a server returning a key it does not control. Registering this ` +
|
|
286
|
+
`set would install one signer where several were intended, so the ` +
|
|
287
|
+
`recovery servers would together carry less weight than the ` +
|
|
288
|
+
`thresholds written alongside them were sized for, and may no longer ` +
|
|
289
|
+
`reach the account's high threshold once the device is lost`,
|
|
290
|
+
);
|
|
291
|
+
Object.setPrototypeOf(this, DuplicateRecoverySignerError.prototype);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export class RecoverySignerEqualsDeviceKeyError extends Error {
|
|
296
|
+
constructor(serverKey: string, signerKey: string) {
|
|
297
|
+
super(
|
|
298
|
+
`Recovery server '${serverKey}' returned the device key ${signerKey} ` +
|
|
299
|
+
`as its signer. The recovery signer must differ from the device key, ` +
|
|
300
|
+
`otherwise the device and the server collapse into a single signer`,
|
|
301
|
+
);
|
|
302
|
+
Object.setPrototypeOf(this, RecoverySignerEqualsDeviceKeyError.prototype);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export class DuplicateAccountSignerError extends Error {
|
|
307
|
+
constructor(address: string) {
|
|
308
|
+
super(
|
|
309
|
+
`Account signer ${address} is listed more than once. Stellar stores ` +
|
|
310
|
+
`signers keyed by public key and SetOptions overwrites rather than ` +
|
|
311
|
+
`accumulates, so repeated entries would install one signer carrying ` +
|
|
312
|
+
`one weight, not several`,
|
|
313
|
+
);
|
|
314
|
+
Object.setPrototypeOf(this, DuplicateAccountSignerError.prototype);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export class SignerKeyEqualsMasterKeyError extends Error {
|
|
319
|
+
constructor(address: string) {
|
|
320
|
+
super(
|
|
321
|
+
`Account signer ${address} is the account's own master key, which ` +
|
|
322
|
+
`cannot be added as a signer of itself`,
|
|
323
|
+
);
|
|
324
|
+
Object.setPrototypeOf(this, SignerKeyEqualsMasterKeyError.prototype);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
277
328
|
export class NoAccountAndNoSponsorError extends Error {
|
|
278
329
|
constructor() {
|
|
279
330
|
super(`Account does not exist and is not sponsored`);
|
|
@@ -21,10 +21,14 @@ import { AccountRecover } from "./AccountRecover";
|
|
|
21
21
|
import { Sep10 } from "../Auth";
|
|
22
22
|
import {
|
|
23
23
|
DeviceKeyEqualsMasterKeyError,
|
|
24
|
+
DuplicateAccountSignerError,
|
|
25
|
+
DuplicateRecoverySignerError,
|
|
24
26
|
NoAccountAndNoSponsorError,
|
|
25
27
|
NoAccountSignersError,
|
|
26
28
|
RecoveryIdentityNotFoundError,
|
|
29
|
+
RecoverySignerEqualsDeviceKeyError,
|
|
27
30
|
ServerRequestFailedError,
|
|
31
|
+
SignerKeyEqualsMasterKeyError,
|
|
28
32
|
} from "../Exceptions";
|
|
29
33
|
import {
|
|
30
34
|
AccountKeypair,
|
|
@@ -44,6 +48,50 @@ type RecoveryParams = {
|
|
|
44
48
|
servers: RecoveryServerMap;
|
|
45
49
|
};
|
|
46
50
|
|
|
51
|
+
/** A signer key paired with the recovery server that returned it. */
|
|
52
|
+
type EnrolledRecoverySigner = {
|
|
53
|
+
serverKey: RecoveryServerKey;
|
|
54
|
+
signerKey: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Assert that a signer set can be installed as written.
|
|
59
|
+
*
|
|
60
|
+
* Stellar stores account signers as a set keyed by public key, and `SetOptions`
|
|
61
|
+
* is an upsert rather than an append — it overwrites a repeated key instead of
|
|
62
|
+
* summing the weights. A set containing the same address twice therefore
|
|
63
|
+
* installs one signer, while the thresholds chosen for the intended set are
|
|
64
|
+
* written alongside it regardless. Because the same transaction also zeroes the
|
|
65
|
+
* master key, repairing that mismatch takes a further `SetOptions`, itself
|
|
66
|
+
* authorized at the account's high threshold — so it is possible only while the
|
|
67
|
+
* remaining signers can still reach `high`, and impossible once they cannot.
|
|
68
|
+
* @param {string} accountAddress - Address of the account whose master key is being locked.
|
|
69
|
+
* @param {AccountSigner[]} accountSigners - The signer set about to be installed.
|
|
70
|
+
* @throws {DuplicateAccountSignerError} If any address appears more than once.
|
|
71
|
+
* @throws {SignerKeyEqualsMasterKeyError} If any address is the account itself.
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*/
|
|
74
|
+
const validateAccountSigners = (
|
|
75
|
+
accountAddress: string,
|
|
76
|
+
accountSigners: AccountSigner[],
|
|
77
|
+
): void => {
|
|
78
|
+
const seen: { [address: string]: true } = {};
|
|
79
|
+
|
|
80
|
+
accountSigners.forEach(({ address }) => {
|
|
81
|
+
const { publicKey } = address;
|
|
82
|
+
|
|
83
|
+
if (publicKey === accountAddress) {
|
|
84
|
+
throw new SignerKeyEqualsMasterKeyError(publicKey);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (seen[publicKey]) {
|
|
88
|
+
throw new DuplicateAccountSignerError(publicKey);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
seen[publicKey] = true;
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
47
95
|
/**
|
|
48
96
|
* Used for Account Recovery using Sep-30.
|
|
49
97
|
* @see {@link https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md}
|
|
@@ -89,8 +137,21 @@ export class Recovery extends AccountRecover {
|
|
|
89
137
|
* **Warning**: This transaction will lock master key of the account. Make sure you have access to
|
|
90
138
|
* specified [RecoverableWalletConfig.deviceAddress]
|
|
91
139
|
*
|
|
140
|
+
* The returned transaction is unsigned — sign it and submit it yourself. It sets signers and
|
|
141
|
+
* thresholds, so it needs signatures meeting the account's current **high** threshold rather
|
|
142
|
+
* than the master key specifically, and the sponsor must sign too when
|
|
143
|
+
* [RecoverableWalletConfig.sponsorAddress] is set. See [RecoverableWallet] for the cases.
|
|
144
|
+
*
|
|
145
|
+
* The signer set returned by the recovery servers is validated before the transaction is built,
|
|
146
|
+
* because undoing the master key lock afterwards takes a further `SetOptions` meeting the
|
|
147
|
+
* account's high threshold, which the collapsed signer set may no longer be able to reach.
|
|
148
|
+
*
|
|
92
149
|
* This transaction can be sponsored.
|
|
93
150
|
* @param {RecoverableWalletConfig} config - The configuration for recoverable wallet.
|
|
151
|
+
* @throws {DeviceKeyEqualsMasterKeyError} If the device address is the account address.
|
|
152
|
+
* @throws {DuplicateRecoverySignerError} If two or more recovery servers return the same signer key.
|
|
153
|
+
* @throws {RecoverySignerEqualsDeviceKeyError} If a recovery server returns the device key.
|
|
154
|
+
* @throws {SignerKeyEqualsMasterKeyError} If a recovery server returns the account's own key.
|
|
94
155
|
* @returns {Promise<RecoverableWallet>} The wallet.
|
|
95
156
|
*/
|
|
96
157
|
async createRecoverableWallet(
|
|
@@ -100,11 +161,15 @@ export class Recovery extends AccountRecover {
|
|
|
100
161
|
throw new DeviceKeyEqualsMasterKeyError();
|
|
101
162
|
}
|
|
102
163
|
|
|
103
|
-
const
|
|
164
|
+
const enrolled = await this.enrollWithRecoveryServer(
|
|
104
165
|
config.accountAddress,
|
|
105
166
|
config.accountIdentity,
|
|
106
167
|
);
|
|
107
168
|
|
|
169
|
+
this.validateEnrolledSigners(enrolled, config.deviceAddress.publicKey);
|
|
170
|
+
|
|
171
|
+
const recoverySigners = enrolled.map(({ signerKey }) => signerKey);
|
|
172
|
+
|
|
108
173
|
const accountSigners: AccountSigner[] = recoverySigners.map((rs) => ({
|
|
109
174
|
address: PublicKeypair.fromPublicKey(rs),
|
|
110
175
|
weight: config.signerWeight.recoveryServer,
|
|
@@ -185,6 +250,9 @@ export class Recovery extends AccountRecover {
|
|
|
185
250
|
* @param {AccountThreshold} accountThreshold - Low, medium, and high thresholds to set on the account.
|
|
186
251
|
* @param {AccountKeypair} [sponsorAddress] - Stellar address of the account sponsoring this transaction.
|
|
187
252
|
* @param {(builder: CommonBuilder) => CommonBuilder} [builderExtra] - Stellar address of the account sponsoring this transaction.
|
|
253
|
+
* @throws {DuplicateAccountSignerError} If the same address appears more than once in [accountSigners].
|
|
254
|
+
* @throws {SignerKeyEqualsMasterKeyError} If [accountSigners] contains [account] itself.
|
|
255
|
+
* @throws {NoAccountAndNoSponsorError} If the account does not exist and no sponsor is given.
|
|
188
256
|
* @returns {Promise<Transaction>} The built transaction.
|
|
189
257
|
*/
|
|
190
258
|
async registerRecoveryServerSigners(
|
|
@@ -194,6 +262,8 @@ export class Recovery extends AccountRecover {
|
|
|
194
262
|
sponsorAddress?: AccountKeypair,
|
|
195
263
|
builderExtra?: (builder: CommonBuilder) => CommonBuilder,
|
|
196
264
|
): Promise<Transaction> {
|
|
265
|
+
validateAccountSigners(account.publicKey, accountSigners);
|
|
266
|
+
|
|
197
267
|
let accountInfo = undefined;
|
|
198
268
|
|
|
199
269
|
try {
|
|
@@ -253,12 +323,14 @@ export class Recovery extends AccountRecover {
|
|
|
253
323
|
* [SEP-30](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md).
|
|
254
324
|
* @param {AccountKeypair} account - Account being registerd.
|
|
255
325
|
* @param {RecoveryIdentityMap} identityMap - map of identities to recovery keys.
|
|
256
|
-
* @returns {Promise<
|
|
326
|
+
* @returns {Promise<EnrolledRecoverySigner[]>} Each recovery signer public key paired with the
|
|
327
|
+
* server that returned it. The pairing is kept so that cross-server validation can name the
|
|
328
|
+
* server responsible when two of them return the same key.
|
|
257
329
|
*/
|
|
258
330
|
private async enrollWithRecoveryServer(
|
|
259
331
|
account: AccountKeypair,
|
|
260
332
|
identityMap: RecoveryIdentityMap,
|
|
261
|
-
): Promise<
|
|
333
|
+
): Promise<EnrolledRecoverySigner[]> {
|
|
262
334
|
return Promise.all(
|
|
263
335
|
Object.keys(this.servers).map(async (key) => {
|
|
264
336
|
const server = this.servers[key];
|
|
@@ -299,11 +371,55 @@ export class Recovery extends AccountRecover {
|
|
|
299
371
|
throw new ServerRequestFailedError(e);
|
|
300
372
|
}
|
|
301
373
|
|
|
302
|
-
return
|
|
374
|
+
return {
|
|
375
|
+
serverKey: key,
|
|
376
|
+
signerKey: this.getLatestRecoverySigner(recoveryAccount.signers),
|
|
377
|
+
};
|
|
303
378
|
}),
|
|
304
379
|
);
|
|
305
380
|
}
|
|
306
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Assert that the recovery servers collectively returned a usable signer set.
|
|
384
|
+
*
|
|
385
|
+
* SEP-30 has each server generate its own unique signing key for the account,
|
|
386
|
+
* so the same key coming back from two servers is a protocol violation, not a
|
|
387
|
+
* judgement call. Left unchecked it halves the recovery signer set while the
|
|
388
|
+
* thresholds sized for the full set are written anyway, which is only
|
|
389
|
+
* discovered when the device is lost and recovery is attempted.
|
|
390
|
+
* @private
|
|
391
|
+
* @param {EnrolledRecoverySigner[]} enrolled - Signer keys paired with the servers that returned them.
|
|
392
|
+
* @param {string} deviceAddress - Address being added as the device signer.
|
|
393
|
+
* @throws {DuplicateRecoverySignerError} If two or more servers returned the same key.
|
|
394
|
+
* @throws {RecoverySignerEqualsDeviceKeyError} If a server returned the device key.
|
|
395
|
+
* @returns {void}
|
|
396
|
+
*/
|
|
397
|
+
private validateEnrolledSigners(
|
|
398
|
+
enrolled: EnrolledRecoverySigner[],
|
|
399
|
+
deviceAddress: string,
|
|
400
|
+
): void {
|
|
401
|
+
const serverKeysBySigner: { [signerKey: string]: RecoveryServerKey[] } = {};
|
|
402
|
+
|
|
403
|
+
enrolled.forEach(({ serverKey, signerKey }) => {
|
|
404
|
+
serverKeysBySigner[signerKey] = [
|
|
405
|
+
...(serverKeysBySigner[signerKey] || []),
|
|
406
|
+
serverKey,
|
|
407
|
+
];
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
Object.keys(serverKeysBySigner).forEach((signerKey) => {
|
|
411
|
+
const serverKeys = serverKeysBySigner[signerKey];
|
|
412
|
+
|
|
413
|
+
if (serverKeys.length > 1) {
|
|
414
|
+
throw new DuplicateRecoverySignerError(serverKeys, signerKey);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (signerKey === deviceAddress) {
|
|
418
|
+
throw new RecoverySignerEqualsDeviceKeyError(serverKeys[0], signerKey);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
307
423
|
private getLatestRecoverySigner(signers: RecoveryAccountSigner[]): string {
|
|
308
424
|
if (signers.length === 0) {
|
|
309
425
|
throw new NoAccountSignersError();
|
|
@@ -25,6 +25,33 @@ export type RecoverableWalletConfig = {
|
|
|
25
25
|
builderExtra?: (builder: CommonBuilder) => CommonBuilder;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The result of registering an account with SEP-30 recovery servers.
|
|
30
|
+
* @property {Transaction} transaction - The **unsigned** registration transaction. The SDK builds
|
|
31
|
+
* it but neither signs nor submits it.
|
|
32
|
+
*
|
|
33
|
+
* It sets signers and thresholds, which Stellar authorizes against the account's **high**
|
|
34
|
+
* threshold — not against the master key specifically:
|
|
35
|
+
*
|
|
36
|
+
* - For an account that already exists, whichever of its current signers together reach `high`
|
|
37
|
+
* can authorize it. The master key is neither automatically sufficient (it may carry less
|
|
38
|
+
* weight than `high`) nor necessarily required (an account whose master key is already locked
|
|
39
|
+
* is enrolled by its other signers alone).
|
|
40
|
+
* - For an account created within this same transaction, i.e. the sponsored path where the
|
|
41
|
+
* account does not exist yet, the new account's own key suffices: a new account starts with
|
|
42
|
+
* master weight 1 and zeroed thresholds.
|
|
43
|
+
* - When `sponsorAddress` is supplied the sponsor must sign as well, being the source of the
|
|
44
|
+
* `beginSponsoringFutureReserves` operation and of `createAccount`.
|
|
45
|
+
*
|
|
46
|
+
* For a newly funded account whose master key is still its only signer, that is just
|
|
47
|
+
* `transaction.sign(accountKp.keypair)` followed by `stellar.submitTransaction(transaction)`.
|
|
48
|
+
*
|
|
49
|
+
* Once submitted the master key is locked, so inspect the transaction before signing if you want
|
|
50
|
+
* to verify the signer set it installs.
|
|
51
|
+
* @property {string[]} signers - Public keys of the recovery server signers added to the account,
|
|
52
|
+
* one per configured server. These are the addresses to pass to `signWithRecoveryServers` when
|
|
53
|
+
* recovering the account later.
|
|
54
|
+
*/
|
|
28
55
|
export type RecoverableWallet = {
|
|
29
56
|
transaction: Transaction;
|
|
30
57
|
signers: string[];
|
|
@@ -47,6 +74,30 @@ export type AccountThreshold = {
|
|
|
47
74
|
high: number;
|
|
48
75
|
};
|
|
49
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Weights assigned to the device signer and to each recovery server signer.
|
|
79
|
+
*
|
|
80
|
+
* These values only mean something relative to {@link AccountThreshold}. Together they decide
|
|
81
|
+
* three separate properties, and it is possible to satisfy some and silently lose the others:
|
|
82
|
+
*
|
|
83
|
+
* 1. The device can operate the account on its own — `device >= high`.
|
|
84
|
+
* 2. The recovery servers can recover the account without the device —
|
|
85
|
+
* `recoveryServer * serverCount >= high`.
|
|
86
|
+
* 3. No single recovery server can act alone — `recoveryServer < low`. This is a choice, not a
|
|
87
|
+
* protocol guarantee: SEP-30 supports both postures. Two or more servers weighted this way
|
|
88
|
+
* means no individual server controls the account, but the spec equally supports a
|
|
89
|
+
* single-server custodial setup where one deliberately does.
|
|
90
|
+
*
|
|
91
|
+
* With two servers, `{ device: 10, recoveryServer: 5 }` against thresholds `{ low: 10, medium: 10,
|
|
92
|
+
* high: 10 }` satisfies all three: the device alone reaches `high`, the two servers together reach
|
|
93
|
+
* `high`, and one server alone reaches nothing.
|
|
94
|
+
*
|
|
95
|
+
* Note that whether the device can act alone is decided by `device` against `high`, per property 1
|
|
96
|
+
* above — not by how `device` compares to `recoveryServer`. A device weighing less than a single
|
|
97
|
+
* recovery server still operates the account independently as long as it meets `high`.
|
|
98
|
+
* @property {number} device - Weight of the device signer, which replaces the master key.
|
|
99
|
+
* @property {number} recoveryServer - Weight given to each recovery server signer.
|
|
100
|
+
*/
|
|
50
101
|
export type SignerWeight = {
|
|
51
102
|
device: number;
|
|
52
103
|
recoveryServer: number;
|
package/test/customer.test.ts
CHANGED
|
@@ -14,7 +14,7 @@ describe("Customer", () => {
|
|
|
14
14
|
await axios.get(
|
|
15
15
|
"https://friendbot.stellar.org/?addr=" + accountKp.publicKey,
|
|
16
16
|
);
|
|
17
|
-
},
|
|
17
|
+
}, 30000);
|
|
18
18
|
|
|
19
19
|
// skipped: the testanchor changed its SEP-12 API behavior
|
|
20
20
|
test.skip("Sep-12 methods work", async () => {
|
|
@@ -89,5 +89,5 @@ describe("Customer", () => {
|
|
|
89
89
|
|
|
90
90
|
// Delete
|
|
91
91
|
await sep12.delete();
|
|
92
|
-
},
|
|
92
|
+
}, 30000);
|
|
93
93
|
});
|