@shuttle.technology/authorization 0.0.0-stage → 0.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/CHANGELOG.md +191 -0
- package/README.md +1124 -2
- package/dist/accountLists.d.ts +28 -0
- package/dist/accountLists.js +27 -0
- package/dist/aesGcmSiv.d.ts +14 -0
- package/dist/aesGcmSiv.js +160 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +189 -0
- package/dist/balances.d.ts +31 -0
- package/dist/balances.js +44 -0
- package/dist/canonicalize.d.ts +19 -0
- package/dist/canonicalize.js +107 -0
- package/dist/client.d.ts +152 -0
- package/dist/client.js +781 -0
- package/dist/clientTypes.d.ts +56 -0
- package/dist/clientTypes.js +5 -0
- package/dist/configuration.d.ts +48 -0
- package/dist/configuration.js +72 -0
- package/dist/consentDebit.d.ts +36 -0
- package/dist/consentDebit.js +95 -0
- package/dist/credentials.d.ts +116 -0
- package/dist/credentials.js +315 -0
- package/dist/debitPermissionWire.d.ts +90 -0
- package/dist/debitPermissionWire.js +118 -0
- package/dist/debitPermissions.d.ts +39 -0
- package/dist/debitPermissions.js +117 -0
- package/dist/deviceKey.d.ts +13 -0
- package/dist/deviceKey.js +28 -0
- package/dist/deviceRecovery.d.ts +40 -0
- package/dist/deviceRecovery.js +143 -0
- package/dist/deviceShare.d.ts +9 -0
- package/dist/deviceShare.js +24 -0
- package/dist/deviceShareEnvelope.d.ts +13 -0
- package/dist/deviceShareEnvelope.js +139 -0
- package/dist/deviceShares.d.ts +91 -0
- package/dist/deviceShares.js +204 -0
- package/dist/errors.d.ts +280 -0
- package/dist/errors.js +85 -0
- package/dist/linkedChecks.d.ts +17 -0
- package/dist/linkedChecks.js +31 -0
- package/dist/linkedRecovery.d.ts +40 -0
- package/dist/linkedRecovery.js +49 -0
- package/dist/linkedRegistrations.d.ts +11 -0
- package/dist/linkedRegistrations.js +109 -0
- package/dist/linkedSendDisplay.d.ts +73 -0
- package/dist/linkedSendDisplay.js +84 -0
- package/dist/linkedWalletSignals.d.ts +22 -0
- package/dist/linkedWalletSignals.js +101 -0
- package/dist/linkedWallets.d.ts +401 -0
- package/dist/linkedWallets.js +436 -0
- package/dist/me.d.ts +27 -0
- package/dist/me.js +30 -0
- package/dist/mediatedOperation.d.ts +27 -0
- package/dist/mediatedOperation.js +68 -0
- package/dist/mobile.d.ts +64 -0
- package/dist/mobile.js +69 -0
- package/dist/modeC.d.ts +131 -0
- package/dist/modeC.js +248 -0
- package/dist/modeCLinked.d.ts +207 -0
- package/dist/modeCLinked.js +270 -0
- package/dist/pages.d.ts +22 -0
- package/dist/pages.js +45 -0
- package/dist/poll.d.ts +15 -0
- package/dist/poll.js +76 -0
- package/dist/redirects.d.ts +45 -0
- package/dist/redirects.js +108 -0
- package/dist/requestGrant.d.ts +169 -0
- package/dist/requestGrant.js +243 -0
- package/dist/sends.d.ts +89 -0
- package/dist/sends.js +127 -0
- package/dist/serviceGrants.d.ts +52 -0
- package/dist/serviceGrants.js +46 -0
- package/dist/slice4.d.ts +187 -0
- package/dist/slice4.js +299 -0
- package/dist/streams.d.ts +212 -0
- package/dist/streams.js +282 -0
- package/dist/trackedWallets.d.ts +53 -0
- package/dist/trackedWallets.js +104 -0
- package/dist/transferStatus.d.ts +45 -0
- package/dist/transferStatus.js +43 -0
- package/dist/walletCreateGrant.d.ts +62 -0
- package/dist/walletCreateGrant.js +89 -0
- package/dist/walletDisplayParts.d.ts +17 -0
- package/dist/walletDisplayParts.js +47 -0
- package/dist/walletEvents.d.ts +43 -0
- package/dist/walletEvents.js +97 -0
- package/dist/walletLimits.d.ts +65 -0
- package/dist/walletLimits.js +102 -0
- package/dist/walletOperations.d.ts +209 -0
- package/dist/walletOperations.js +317 -0
- package/dist/webRenewal.d.ts +92 -0
- package/dist/webRenewal.js +179 -0
- package/package.json +65 -3
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// HTTP-only account links and explicit linked streams.register. Provider tokens
|
|
2
|
+
// are arguments of individual writes; this facade never retains them.
|
|
3
|
+
//
|
|
4
|
+
// The account's session authorizes what is done with a linked wallet: its
|
|
5
|
+
// registration for tracking (register() comes back authorized and
|
|
6
|
+
// executeRegistration() follows at once), and a send from it (send() and
|
|
7
|
+
// replaceSend() come back authorized, prepare() has the transfer prepared and
|
|
8
|
+
// shown, executeSend() sends it on the owner's confirm). No passkey is asked
|
|
9
|
+
// for either (owner decisions of 8 and 14 October 2026); a passkey still
|
|
10
|
+
// confirms creating a wallet.
|
|
11
|
+
import { ClientFlowError, canonicalDecimal, integer, opaqueId, record, rfc3339UTC, unexpiredRFC3339 } from './auth.js';
|
|
12
|
+
import { parseWalletEventPage } from './streams.js';
|
|
13
|
+
export { resumeCursor } from './streams.js';
|
|
14
|
+
import { parseOperationEnvelope } from './slice4.js';
|
|
15
|
+
import { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.js';
|
|
16
|
+
export { linkedEventsPath, linkedRegistrationsPath, parseLinkedRegistrationOperation, parseLinkedRegistrationOperationEnvelope, parseLinkedRegistrationResult, parseLinkedStreamsRegistrationList, validateLinkedRegistrationCreate } from './linkedRegistrations.js';
|
|
17
|
+
import { linkedSignalsPath, readLinkedWalletSignals } from './linkedWalletSignals.js';
|
|
18
|
+
export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets } from './linkedWalletSignals.js';
|
|
19
|
+
import { parseTransferStatus, transferStatusPath } from './transferStatus.js';
|
|
20
|
+
import { parseLinkedWalletBalances } from './balances.js';
|
|
21
|
+
export { parseLinkedWalletBalances } from './balances.js';
|
|
22
|
+
import { parseSendPage, sendsPath } from './sends.js';
|
|
23
|
+
export { eventOfTransaction, parseSendPage, sendEvents, sendsPath } from './sends.js';
|
|
24
|
+
export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates } from './transferStatus.js';
|
|
25
|
+
import { parseWalletLimits, walletLimitsCreate } from './walletLimits.js';
|
|
26
|
+
export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients } from './walletLimits.js';
|
|
27
|
+
import { DebitPermissions } from './debitPermissions.js';
|
|
28
|
+
export { DebitPermissions, MaxDebitAssets, MaxDebitRecipients } from './debitPermissions.js';
|
|
29
|
+
import { b64, fault, hex, network, publicKey, requestID, requestUUID, safeRevision, text, token, uuid } from './linkedChecks.js';
|
|
30
|
+
import { linkedSendAsset, parseUserWalletOperationEnvelope, replaceAction, transferComment } from './walletOperations.js';
|
|
31
|
+
export class LinkedWallets {
|
|
32
|
+
#transport;
|
|
33
|
+
constructor(transport) { this.#transport = transport; }
|
|
34
|
+
async challenge(cognitoAccessToken, credentialId, signal) {
|
|
35
|
+
token(cognitoAccessToken);
|
|
36
|
+
requestID(credentialId);
|
|
37
|
+
const { payload } = await this.#transport.write('/v1/wallet-connections/challenges', 'POST', { cognito_access_token: cognitoAccessToken, passkey_credential_id: credentialId }, signal);
|
|
38
|
+
const challenge = parseWalletLinkChallenge(payload, this.#transport.now());
|
|
39
|
+
if (challenge.publicKey.allowCredentials[0].id !== credentialId)
|
|
40
|
+
fault();
|
|
41
|
+
return challenge;
|
|
42
|
+
}
|
|
43
|
+
async connect(challengeId, cognitoAccessToken, assertion, signal) {
|
|
44
|
+
requestUUID(challengeId);
|
|
45
|
+
token(cognitoAccessToken);
|
|
46
|
+
validateWalletLinkAssertion(assertion);
|
|
47
|
+
const encoded = JSON.stringify(assertion);
|
|
48
|
+
if (new TextEncoder().encode(encoded).length > 65536)
|
|
49
|
+
throw new ClientFlowError('invalid_linked_wallet_request');
|
|
50
|
+
return parseWalletConnection((await this.#transport.write('/v1/wallet-connections', 'POST', { challenge_id: challengeId, cognito_access_token: cognitoAccessToken, response: encoded }, signal)).payload);
|
|
51
|
+
}
|
|
52
|
+
async connection(connectionId, signal) {
|
|
53
|
+
requestUUID(connectionId);
|
|
54
|
+
const connection = parseWalletConnection(await this.#transport.get(`/v1/wallet-connections/${connectionId}`, signal));
|
|
55
|
+
if (connection.id !== connectionId)
|
|
56
|
+
fault();
|
|
57
|
+
return connection;
|
|
58
|
+
}
|
|
59
|
+
async sync(connectionId, cognitoAccessToken, signal) {
|
|
60
|
+
requestUUID(connectionId);
|
|
61
|
+
token(cognitoAccessToken);
|
|
62
|
+
const connection = parseWalletConnection((await this.#transport.write(`/v1/wallet-connections/${connectionId}/sync`, 'POST', { cognito_access_token: cognitoAccessToken }, signal)).payload);
|
|
63
|
+
if (connection.id !== connectionId)
|
|
64
|
+
fault();
|
|
65
|
+
return connection;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* refresh binds the account to the Wallet identity the provider owns for it
|
|
69
|
+
* and synchronizes its public wallet inventory. It carries no Wallet token
|
|
70
|
+
* and no body; repeating it converges on the same connection. Call it after
|
|
71
|
+
* a wallet was created so the wallet appears in list().
|
|
72
|
+
*/
|
|
73
|
+
async refresh(signal) {
|
|
74
|
+
const connection = parseWalletConnection((await this.#transport.write('/v1/wallet-connections/brokered', 'POST', undefined, signal)).payload);
|
|
75
|
+
if (connection.state !== 'connected')
|
|
76
|
+
fault();
|
|
77
|
+
return connection;
|
|
78
|
+
}
|
|
79
|
+
async disconnect(connectionId, signal) {
|
|
80
|
+
requestUUID(connectionId);
|
|
81
|
+
const connection = parseWalletConnection((await this.#transport.write(`/v1/wallet-connections/${connectionId}`, 'DELETE', undefined, signal)).payload);
|
|
82
|
+
if (connection.id !== connectionId || connection.state !== 'revoked')
|
|
83
|
+
fault();
|
|
84
|
+
return connection;
|
|
85
|
+
}
|
|
86
|
+
async list(signal) {
|
|
87
|
+
return parseLinkedWalletList(await this.#transport.get('/v1/linked-wallets', signal));
|
|
88
|
+
}
|
|
89
|
+
async wallet(linkedWalletId, signal) {
|
|
90
|
+
requestUUID(linkedWalletId);
|
|
91
|
+
const root = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}`, signal), ['request_id', 'wallet']);
|
|
92
|
+
if (!opaqueId(root.request_id))
|
|
93
|
+
fault();
|
|
94
|
+
const wallet = parseLinkedWallet(root.wallet);
|
|
95
|
+
if (wallet.id !== linkedWalletId)
|
|
96
|
+
fault();
|
|
97
|
+
return { requestId: root.request_id, wallet };
|
|
98
|
+
}
|
|
99
|
+
/** Every metadata page is fresh; restart at the beginning to discover earlier insertions. */
|
|
100
|
+
async registrations(linkedWalletId, options = {}) {
|
|
101
|
+
const path = linkedRegistrationsPath(linkedWalletId, options);
|
|
102
|
+
const page = parseLinkedStreamsRegistrationList(await this.#transport.get(path, options.signal), linkedWalletId);
|
|
103
|
+
if (page.items.length > (options.limit ?? 50) || (options.afterRegistrationId !== undefined && page.items.some(item => item.registrationId <= options.afterRegistrationId)))
|
|
104
|
+
fault();
|
|
105
|
+
return page;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One page of the linked wallet's retained events as wallet events 3.0:
|
|
109
|
+
* what came in, what went out and what else happened, each with kind,
|
|
110
|
+
* status and revision, those of the kinds eventTypes names. Continuations
|
|
111
|
+
* are sealed opaque cursors.
|
|
112
|
+
*/
|
|
113
|
+
async events(linkedWalletId, options = {}) {
|
|
114
|
+
return parseWalletEventPage(await this.#transport.get(linkedEventsPath(linkedWalletId, options), options.signal));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Opens one signal stream for these linked wallets and reads it to its end.
|
|
118
|
+
* onOpen says the stream is live: read every wallet once then. onWallets
|
|
119
|
+
* names wallets with new events: read them with events(). The promise
|
|
120
|
+
* resolves with the reason the stream ended; a stream lives no longer than
|
|
121
|
+
* an access token, so the caller opens the next one. It rejects with the
|
|
122
|
+
* server's refusal, with the abort reason, or when the connection fails;
|
|
123
|
+
* keep an ordinary timed read for those times.
|
|
124
|
+
*/
|
|
125
|
+
async signals(linkedWalletIds, handlers) {
|
|
126
|
+
const body = await this.#transport.stream(linkedSignalsPath(linkedWalletIds), handlers.signal);
|
|
127
|
+
return readLinkedWalletSignals(body, linkedWalletIds, handlers);
|
|
128
|
+
}
|
|
129
|
+
async createRegistration(create, idempotencyKey, signal) {
|
|
130
|
+
validateLinkedRegistrationCreate(create, this.#transport.accountId());
|
|
131
|
+
requestID(idempotencyKey);
|
|
132
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
133
|
+
const operation = parseLinkedRegistrationOperationEnvelope(payload).operation;
|
|
134
|
+
if (operation.ownerId !== accountId || operation.linkedWalletId !== create.linked_wallet_id)
|
|
135
|
+
fault();
|
|
136
|
+
return operation;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Starts tracking a linked wallet on one network. An EVM wallet has one
|
|
140
|
+
* address on every EVM network, so it is registered on each of them with its
|
|
141
|
+
* own call. activityTypes names the kinds of activity events asked for beside
|
|
142
|
+
* deposits; omitted or empty means deposits only, and Streams refuses a kind
|
|
143
|
+
* the network does not have. The operation comes back authorized;
|
|
144
|
+
* executeRegistration() with its version dispatches it.
|
|
145
|
+
*/
|
|
146
|
+
register(input, idempotencyKey, signal) {
|
|
147
|
+
record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes']);
|
|
148
|
+
const create = { kind: 'streams.register', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: input.linkedWalletId, payload: { network: input.network } };
|
|
149
|
+
if (input.assets !== undefined)
|
|
150
|
+
create.payload.assets = input.assets;
|
|
151
|
+
// An empty selection means deposits only and is not sent; any other value is checked with the whole request.
|
|
152
|
+
if (input.activityTypes !== undefined && !(Array.isArray(input.activityTypes) && input.activityTypes.length === 0))
|
|
153
|
+
create.payload.activity_types = input.activityTypes;
|
|
154
|
+
return this.createRegistration(create, idempotencyKey, signal);
|
|
155
|
+
}
|
|
156
|
+
async operation(operationId, signal) {
|
|
157
|
+
requestID(operationId);
|
|
158
|
+
const operation = parseLinkedRegistrationOperationEnvelope(await this.#transport.get(`/v1/operations/${operationId}`, signal)).operation;
|
|
159
|
+
if (operation.id !== operationId)
|
|
160
|
+
fault();
|
|
161
|
+
return operation;
|
|
162
|
+
}
|
|
163
|
+
async executeRegistration(operationId, version, idempotencyKey, signal) {
|
|
164
|
+
requestID(operationId);
|
|
165
|
+
requestID(idempotencyKey);
|
|
166
|
+
if (!canonicalDecimal(version))
|
|
167
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
168
|
+
const operation = parseLinkedRegistrationOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', undefined, signal, idempotencyKey, version)).payload).operation;
|
|
169
|
+
if (operation.id !== operationId)
|
|
170
|
+
fault();
|
|
171
|
+
return operation;
|
|
172
|
+
}
|
|
173
|
+
async registrationResult(operationId, signal) {
|
|
174
|
+
requestID(operationId);
|
|
175
|
+
return parseLinkedRegistrationResult(await this.#transport.get(`/v1/operations/${operationId}/result`, signal), operationId);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Starts a send from a linked wallet. The account's session authorizes it:
|
|
179
|
+
* the operation comes back authorized (authorizationBasis
|
|
180
|
+
* session_authorized), with no passkey and no confirmation window. Then
|
|
181
|
+
* prepare() has the own-wallet system prepare the transfer and name its
|
|
182
|
+
* fee; show the owner what was prepared and, on their confirm,
|
|
183
|
+
* executeSend() with the device share.
|
|
184
|
+
*/
|
|
185
|
+
async send(input, idempotencyKey, signal) {
|
|
186
|
+
const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment']);
|
|
187
|
+
requestID(idempotencyKey);
|
|
188
|
+
if (!uuid(declared.linkedWalletId) || !network(declared.network) || !linkedSendAsset(declared.asset) || typeof declared.recipient !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(declared.recipient) ||
|
|
189
|
+
!canonicalDecimal(declared.amountRaw) || declared.amountRaw === '0' || declared.amountRaw.length > 78 ||
|
|
190
|
+
// Only a TON transfer carries a comment.
|
|
191
|
+
(declared.comment !== undefined && (!transferComment(declared.comment) || !declared.network.startsWith('ton:'))))
|
|
192
|
+
throw new ClientFlowError('invalid_operation_create');
|
|
193
|
+
const sent = { network: declared.network, asset: declared.asset, recipient: declared.recipient, amount_raw: declared.amountRaw };
|
|
194
|
+
if (declared.comment !== undefined)
|
|
195
|
+
sent.comment = declared.comment;
|
|
196
|
+
const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId, payload: sent };
|
|
197
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
198
|
+
const operation = parseUserWalletOperationEnvelope(payload).operation;
|
|
199
|
+
const shown = operation.display;
|
|
200
|
+
if (operation.kind !== 'wallet.send' || operation.ownerId !== accountId || operation.linkedWallet?.linkedWalletId !== declared.linkedWalletId ||
|
|
201
|
+
shown.asset !== declared.asset || shown.comment !== declared.comment)
|
|
202
|
+
fault();
|
|
203
|
+
return operation;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Replaces a send the own-wallet system broadcast on an EVM network and the
|
|
207
|
+
* network has not included: "speed_up" sends the same transfer again with a
|
|
208
|
+
* higher fee, "cancel" sends nothing to the wallet's own address in its
|
|
209
|
+
* place. The answer is a send of its own, authorized by the session like
|
|
210
|
+
* send(): prepare() has the own-wallet system prepare the replacement (it
|
|
211
|
+
* may answer that the transfer no longer waits), show it with its new fee,
|
|
212
|
+
* and on the owner's confirm executeSend(). Whichever of the two
|
|
213
|
+
* transactions the network includes, the other fails; read
|
|
214
|
+
* transferStatus() of both.
|
|
215
|
+
*/
|
|
216
|
+
async replaceSend(input, idempotencyKey, signal) {
|
|
217
|
+
const declared = record(input, ['linkedWalletId', 'operationId', 'action']);
|
|
218
|
+
requestID(idempotencyKey);
|
|
219
|
+
if (!uuid(declared.linkedWalletId) || !opaqueId(declared.operationId) || !replaceAction(declared.action))
|
|
220
|
+
throw new ClientFlowError('invalid_operation_create');
|
|
221
|
+
const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId,
|
|
222
|
+
payload: { action: declared.action, replaces_operation_id: declared.operationId } };
|
|
223
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
224
|
+
const operation = parseUserWalletOperationEnvelope(payload).operation;
|
|
225
|
+
const shown = operation.display;
|
|
226
|
+
if (operation.kind !== 'wallet.send' || operation.ownerId !== accountId || operation.linkedWallet?.linkedWalletId !== declared.linkedWalletId ||
|
|
227
|
+
shown.replacesOperationId !== declared.operationId || shown.replaceAction !== declared.action)
|
|
228
|
+
fault();
|
|
229
|
+
return operation;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* What the wallet holds on each of its networks: the coin and the tokens
|
|
233
|
+
* the stand names there, in minimal units with their decimals. The provider
|
|
234
|
+
* reads its own stand's nodes. A network whose node gave no amount is in
|
|
235
|
+
* the answer with its status and without amounts; it fails nothing.
|
|
236
|
+
*/
|
|
237
|
+
async balances(linkedWalletId, signal) {
|
|
238
|
+
requestUUID(linkedWalletId);
|
|
239
|
+
return parseLinkedWalletBalances(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/balances`, signal), linkedWalletId);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Has the own-wallet system prepare the transfer of a send or replacement
|
|
243
|
+
* the session authorized, and returns the send with what was prepared:
|
|
244
|
+
* display.preparedSummary, the sending address and the fee estimate. Show
|
|
245
|
+
* them to the owner; executeSend() sends exactly that transfer. The
|
|
246
|
+
* transfer is prepared once: calling again returns the same prepared send
|
|
247
|
+
* and asks the own-wallet system nothing. A prepared send expires within
|
|
248
|
+
* 150 seconds; one that expired is sent again from send(). The request
|
|
249
|
+
* carries no body, idempotency key or version.
|
|
250
|
+
*/
|
|
251
|
+
async prepare(operationId, signal) {
|
|
252
|
+
requestID(operationId);
|
|
253
|
+
const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/prepare`, 'POST', undefined, signal)).payload).operation;
|
|
254
|
+
if (operation.id !== operationId || operation.kind !== 'wallet.send' || operation.linkedWallet === undefined)
|
|
255
|
+
fault();
|
|
256
|
+
return operation;
|
|
257
|
+
}
|
|
258
|
+
/** The key the device seals its share to, read right before executeSend(). */
|
|
259
|
+
async transportKey(linkedWalletId, signal) {
|
|
260
|
+
requestUUID(linkedWalletId);
|
|
261
|
+
const r = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/transport-key`, signal), ['request_id', 'public_key', 'encryption_algorithm']);
|
|
262
|
+
if (!opaqueId(r.request_id) || typeof r.public_key !== 'string' || !/^(0[23][0-9a-f]{64}|04[0-9a-f]{128})$/.test(r.public_key) || typeof r.encryption_algorithm !== 'string' || !/^[a-z0-9_-]{1,128}$/.test(r.encryption_algorithm))
|
|
263
|
+
fault();
|
|
264
|
+
return { requestId: r.request_id, publicKey: r.public_key, encryptionAlgorithm: r.encryption_algorithm };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Executes a prepared send, on the owner's confirm, with the device share
|
|
268
|
+
* resealed for the own-wallet system. Without deviceShare the SDK supplies
|
|
269
|
+
* the share it keeps (deviceShareFor()), recovered first with one passkey
|
|
270
|
+
* confirmation when this browser holds none; an app that keeps its own share
|
|
271
|
+
* passes it resealed (resealDeviceShare). The provider cannot open the share.
|
|
272
|
+
* A send never prepared is refused with VERSION_CONFLICT, an expired one
|
|
273
|
+
* with OPERATION_EXPIRED.
|
|
274
|
+
*/
|
|
275
|
+
async executeSend(operationId, version, idempotencyKey, deviceShare, signal) {
|
|
276
|
+
requestID(operationId);
|
|
277
|
+
requestID(idempotencyKey);
|
|
278
|
+
if (!canonicalDecimal(version))
|
|
279
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
280
|
+
if (deviceShare === undefined) {
|
|
281
|
+
const linkedWalletId = parseUserWalletOperationEnvelope(await this.#transport.get(`/v1/operations/${operationId}`, signal)).operation.linkedWallet?.linkedWalletId;
|
|
282
|
+
if (linkedWalletId === undefined)
|
|
283
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
284
|
+
deviceShare = await this.deviceShareFor(linkedWalletId, signal);
|
|
285
|
+
}
|
|
286
|
+
const share = record(deviceShare, ['ciphertext', 'nonce', 'ephemeralPublicKey', 'encryptionAlgorithm', 'encryptionContext']);
|
|
287
|
+
if (![share.ciphertext, share.nonce, share.ephemeralPublicKey, share.encryptionContext].every(hex) || typeof share.encryptionAlgorithm !== 'string')
|
|
288
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
289
|
+
const body = { device_share: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeral_public_key: share.ephemeralPublicKey, encryption_algorithm: share.encryptionAlgorithm, encryption_context: share.encryptionContext } };
|
|
290
|
+
const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', body, signal, idempotencyKey, version)).payload).operation;
|
|
291
|
+
if (operation.id !== operationId)
|
|
292
|
+
fault();
|
|
293
|
+
return operation;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The device share the SDK keeps for this wallet, resealed to the
|
|
297
|
+
* own-wallet system's transport key read right before: what executeSend()
|
|
298
|
+
* sends and what a Mode C request is signed with (signLinkedMediatedRequest).
|
|
299
|
+
* A browser that holds no share of the wallet (another domain, cleared site
|
|
300
|
+
* data, another browser or device) recovers it first: the person confirms
|
|
301
|
+
* once with the passkey on the provider's page, and the share the wallet had
|
|
302
|
+
* elsewhere stops working. Two tabs never recover the same wallet at once.
|
|
303
|
+
*/
|
|
304
|
+
async deviceShareFor(linkedWalletId, signal) {
|
|
305
|
+
return this.#sealedTo(linkedWalletId, async () => (await this.transportKey(linkedWalletId, signal)).publicKey, signal);
|
|
306
|
+
}
|
|
307
|
+
/** The kept share, recovered first when this browser holds none (with recover false: refused), resealed to the key read right before. */
|
|
308
|
+
async #sealedTo(linkedWalletId, publicKey, signal, recover = true) {
|
|
309
|
+
const devices = this.#devices();
|
|
310
|
+
const { wallet } = await this.wallet(linkedWalletId, signal);
|
|
311
|
+
if (recover)
|
|
312
|
+
await devices.ensure(wallet, this.#serverKey(linkedWalletId, signal), { signal });
|
|
313
|
+
else if (!(await devices.has(wallet, this.#serverKey(linkedWalletId, signal))))
|
|
314
|
+
throw new ClientFlowError('device_share_unavailable');
|
|
315
|
+
return devices.sealedFor(wallet, await publicKey());
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The device key of the wallet's latest creation or recovery the provider
|
|
319
|
+
* made, as 33-byte compressed hex; absent when it knows none. A share kept
|
|
320
|
+
* here that is sealed to another key was replaced by a recovery elsewhere.
|
|
321
|
+
*/
|
|
322
|
+
async deviceKey(linkedWalletId, signal) {
|
|
323
|
+
requestUUID(linkedWalletId);
|
|
324
|
+
const r = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/device-key`, signal), ['request_id'], ['device_public_key']);
|
|
325
|
+
if (!opaqueId(r.request_id) || (r.device_public_key !== undefined && (typeof r.device_public_key !== 'string' || !/^0[23][0-9a-f]{64}$/.test(r.device_public_key))))
|
|
326
|
+
fault();
|
|
327
|
+
return r.device_public_key === undefined ? { requestId: r.request_id } : { requestId: r.request_id, devicePublicKey: r.device_public_key };
|
|
328
|
+
}
|
|
329
|
+
/** Whether this browser holds a current device share of the wallet; without one the next send recovers it with the passkey. */
|
|
330
|
+
async hasDeviceShare(linkedWalletId, signal) {
|
|
331
|
+
const devices = this.#devices();
|
|
332
|
+
return devices.has((await this.wallet(linkedWalletId, signal)).wallet, this.#serverKey(linkedWalletId, signal));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Recovers the wallet's device share onto this browser when it holds no
|
|
336
|
+
* current one, as deviceShareFor() would, for an app that does it ahead of a
|
|
337
|
+
* transfer; force recovers even then. credentialId names the passkey that
|
|
338
|
+
* confirms it, by default the account's newest. Answers whether a recovery
|
|
339
|
+
* ran. Optional: sends recover by themselves.
|
|
340
|
+
*/
|
|
341
|
+
async recoverDevice(linkedWalletId, options = {}) {
|
|
342
|
+
const devices = this.#devices();
|
|
343
|
+
return devices.ensure((await this.wallet(linkedWalletId, options.signal)).wallet, this.#serverKey(linkedWalletId, options.signal), options);
|
|
344
|
+
}
|
|
345
|
+
#serverKey(linkedWalletId, signal) {
|
|
346
|
+
return async () => (await this.deviceKey(linkedWalletId, signal)).devicePublicKey;
|
|
347
|
+
}
|
|
348
|
+
#devices() {
|
|
349
|
+
if (!this.#transport.devices)
|
|
350
|
+
throw new ClientFlowError('device_share_unavailable');
|
|
351
|
+
return this.#transport.devices;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* What became of the transfer a send prepared: on its way, confirmed by the
|
|
355
|
+
* network, or failed and why. The send's operation succeeds when the
|
|
356
|
+
* own-wallet system accepted the transfer; this is what the network did
|
|
357
|
+
* with it. Read it until final is true. Before the send was prepared
|
|
358
|
+
* there is no transfer and the read is refused with NOT_FOUND.
|
|
359
|
+
*/
|
|
360
|
+
async transferStatus(operationId, signal) {
|
|
361
|
+
return parseTransferStatus(await this.#transport.get(transferStatusPath(operationId), signal), operationId);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* One page of the account's send history, newest first: every wallet.send
|
|
365
|
+
* the account can read, with its transaction hash and the recorded final
|
|
366
|
+
* fate of its transfer. A send with a hash and no transfer has not been
|
|
367
|
+
* reported final yet: read transferStatus() for it, which records the fate.
|
|
368
|
+
* The cursor carries the first page's limit.
|
|
369
|
+
*/
|
|
370
|
+
async sends(options) {
|
|
371
|
+
const { path, signal } = sendsPath(options);
|
|
372
|
+
return parseSendPage(await this.#transport.get(path, signal));
|
|
373
|
+
}
|
|
374
|
+
/** The limits every send from the wallet obeys, read with the session (walletLimits.ts). Revision "0": never set. */
|
|
375
|
+
async limits(linkedWalletId, signal) {
|
|
376
|
+
requestUUID(linkedWalletId);
|
|
377
|
+
return parseWalletLimits(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/limits`, signal), linkedWalletId);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Asks to replace the wallet's limits as a whole; empty ceilings and recipients remove them all. The operation
|
|
381
|
+
* awaits the owner's passkey: the client's challengeOperation(), completeOperationConfirmation() and
|
|
382
|
+
* executeOperation() apply it. A send the limits refuse answers one of WalletLimitRefusals.
|
|
383
|
+
*/
|
|
384
|
+
async setLimits(linkedWalletId, limits, idempotencyKey, signal) {
|
|
385
|
+
const create = walletLimitsCreate(this.#transport.accountId(), linkedWalletId, limits);
|
|
386
|
+
requestID(idempotencyKey);
|
|
387
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
388
|
+
const operation = parseOperationEnvelope(payload).operation;
|
|
389
|
+
if (operation.display.kind !== 'wallet_limits.set' || operation.ownerId !== accountId || operation.display.linkedWalletId !== linkedWalletId)
|
|
390
|
+
fault();
|
|
391
|
+
return operation;
|
|
392
|
+
}
|
|
393
|
+
/** The permissions the account lets services debit its linked wallets by, without the device (debitPermissions.ts). */
|
|
394
|
+
debitPermissions() { return new DebitPermissions(this.#transport, (linkedWalletId, keeperKey, signal, recover) => this.#sealedTo(linkedWalletId, async () => keeperKey, signal, recover)); }
|
|
395
|
+
}
|
|
396
|
+
export function parseWalletConnection(payload) {
|
|
397
|
+
const r = record(payload, ['request_id', 'id', 'connector_id', 'state', 'created_at', 'last_synced_at'], ['revoked_at']);
|
|
398
|
+
if (!opaqueId(r.request_id) || !uuid(r.id) || !opaqueId(r.connector_id) || !['connected', 'revoked', 'reauth_required'].includes(r.state) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.last_synced_at) || (r.state === 'revoked') !== (r.revoked_at !== undefined) || (r.revoked_at !== undefined && !rfc3339UTC(r.revoked_at)))
|
|
399
|
+
fault();
|
|
400
|
+
const out = { requestId: r.request_id, id: r.id, connectorId: r.connector_id, state: r.state, createdAt: r.created_at, lastSyncedAt: r.last_synced_at };
|
|
401
|
+
if (r.revoked_at !== undefined)
|
|
402
|
+
out.revokedAt = r.revoked_at;
|
|
403
|
+
return Object.freeze(out);
|
|
404
|
+
}
|
|
405
|
+
export function parseLinkedWallet(payload) {
|
|
406
|
+
const r = record(payload, ['id', 'connection_id', 'external_wallet_id', 'external_key_id', 'network', 'address', 'algorithm', 'public_key', 'binding_revision', 'status', 'observed_at']);
|
|
407
|
+
if (![r.id, r.connection_id, r.external_wallet_id, r.external_key_id].every(uuid) || !text(r.network, 64) || !text(r.address, 256) || !safeRevision(r.binding_revision) || !['active', 'inactive'].includes(r.status) || !rfc3339UTC(r.observed_at) || !publicKey(r.algorithm, r.public_key))
|
|
408
|
+
fault();
|
|
409
|
+
return Object.freeze({ id: r.id, connectionId: r.connection_id, externalWalletId: r.external_wallet_id, externalKeyId: r.external_key_id, network: r.network, address: r.address, algorithm: r.algorithm, publicKey: r.public_key, bindingRevision: r.binding_revision, status: r.status, observedAt: r.observed_at });
|
|
410
|
+
}
|
|
411
|
+
export function parseLinkedWalletList(payload) {
|
|
412
|
+
const r = record(payload, ['request_id', 'items']);
|
|
413
|
+
if (!opaqueId(r.request_id) || !Array.isArray(r.items) || r.items.length > 1000)
|
|
414
|
+
fault();
|
|
415
|
+
const items = r.items.map(parseLinkedWallet);
|
|
416
|
+
if (items.some(w => w.status !== 'active') || new Set(items.map(w => w.id)).size !== items.length)
|
|
417
|
+
fault();
|
|
418
|
+
return { requestId: r.request_id, items: Object.freeze(items) };
|
|
419
|
+
}
|
|
420
|
+
export function parseWalletLinkChallenge(payload, now) {
|
|
421
|
+
const r = record(payload, ['request_id', 'challenge_id', 'expires_at', 'public_key']);
|
|
422
|
+
const p = record(r.public_key, ['allow_credentials', 'challenge', 'rp_id', 'timeout_ms', 'user_verification']);
|
|
423
|
+
if (!opaqueId(r.request_id) || !uuid(r.challenge_id) || !unexpiredRFC3339(r.expires_at, now) || !b64(p.challenge) || !text(p.rp_id, 253) || !integer(p.timeout_ms) || p.timeout_ms < 1 || p.timeout_ms > 300000 || p.user_verification !== 'required' || !Array.isArray(p.allow_credentials) || p.allow_credentials.length !== 1)
|
|
424
|
+
fault();
|
|
425
|
+
const c = record(p.allow_credentials[0], ['id', 'type']);
|
|
426
|
+
if (c.type !== 'public-key' || !b64(c.id))
|
|
427
|
+
fault();
|
|
428
|
+
return { requestId: r.request_id, challengeId: r.challenge_id, expiresAt: r.expires_at, publicKey: { challenge: p.challenge, rpId: p.rp_id, timeoutMs: p.timeout_ms, userVerification: 'required', allowCredentials: [{ id: c.id, type: 'public-key' }] } };
|
|
429
|
+
}
|
|
430
|
+
export function validateWalletLinkAssertion(value) {
|
|
431
|
+
const r = record(value, ['id', 'rawId', 'type', 'response']);
|
|
432
|
+
const p = record(r.response, ['clientDataJSON', 'authenticatorData', 'signature', 'userHandle']);
|
|
433
|
+
if (r.type !== 'public-key' || !b64(r.id) || r.rawId !== r.id || ![p.clientDataJSON, p.authenticatorData, p.signature].every(b64) || (p.userHandle !== '' && !b64(p.userHandle)))
|
|
434
|
+
throw new ClientFlowError('invalid_linked_wallet_request');
|
|
435
|
+
return value;
|
|
436
|
+
}
|
package/dist/me.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One wallet of the account: "wallet" is a wallet the provider created,
|
|
3
|
+
* "linked_wallet" one its owner connected, and walletId is an identifier of
|
|
4
|
+
* that kind. A listed linked wallet is active; an own wallet is active or
|
|
5
|
+
* disabled.
|
|
6
|
+
*/
|
|
7
|
+
export interface MeWallet {
|
|
8
|
+
walletId: string;
|
|
9
|
+
kind: "wallet" | "linked_wallet";
|
|
10
|
+
network: string;
|
|
11
|
+
address: string;
|
|
12
|
+
state: "active" | "disabled";
|
|
13
|
+
}
|
|
14
|
+
export interface MeProjection {
|
|
15
|
+
requestId: string;
|
|
16
|
+
account: {
|
|
17
|
+
accountId: string;
|
|
18
|
+
version: string;
|
|
19
|
+
setupRequired: boolean;
|
|
20
|
+
};
|
|
21
|
+
sessionId: string;
|
|
22
|
+
serverTime: number;
|
|
23
|
+
/** Every wallet of the account, own then linked, at most 1000 of each kind. */
|
|
24
|
+
wallets: MeWallet[];
|
|
25
|
+
}
|
|
26
|
+
/** parseMeProjection reads exactly one `/v1/me` answer. */
|
|
27
|
+
export declare function parseMeProjection(payload: unknown): MeProjection;
|
package/dist/me.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live `/v1/me` projection: the current account, the session, the server
|
|
3
|
+
* time and every wallet of the account. It is read as strictly as every
|
|
4
|
+
* other answer: an unknown member, a missing one or a value outside its
|
|
5
|
+
* closed form refuses the whole answer.
|
|
6
|
+
*/
|
|
7
|
+
import { ClientFlowError, canonicalDecimal, integer, opaqueId, record } from "./auth.js";
|
|
8
|
+
/** The most wallets of one kind an account lists; the answer holds two kinds. */
|
|
9
|
+
const maxWalletsPerKind = 1000;
|
|
10
|
+
/** parseMeProjection reads exactly one `/v1/me` answer. */
|
|
11
|
+
export function parseMeProjection(payload) {
|
|
12
|
+
const root = record(payload, ["request_id", "account", "session_id", "server_time", "wallets"]);
|
|
13
|
+
const account = record(root.account, ["account_id", "version", "setup_required"]);
|
|
14
|
+
if (!opaqueId(root.request_id) || !opaqueId(account.account_id) || !canonicalDecimal(account.version) || typeof account.setup_required !== "boolean" || !opaqueId(root.session_id) || !integer(root.server_time) ||
|
|
15
|
+
!Array.isArray(root.wallets) || root.wallets.length > 2 * maxWalletsPerKind) {
|
|
16
|
+
throw new ClientFlowError("invalid_server_response");
|
|
17
|
+
}
|
|
18
|
+
const wallets = root.wallets.map((value) => {
|
|
19
|
+
const item = record(value, ["wallet_id", "kind", "network", "address", "state"]);
|
|
20
|
+
if (!opaqueId(item.wallet_id) || (item.kind !== "wallet" && item.kind !== "linked_wallet") || typeof item.network !== "string" || item.network === "" ||
|
|
21
|
+
typeof item.address !== "string" || item.address === "" || item.address.length > 256 || (item.state !== "active" && item.state !== "disabled")) {
|
|
22
|
+
throw new ClientFlowError("invalid_server_response");
|
|
23
|
+
}
|
|
24
|
+
return { walletId: item.wallet_id, kind: item.kind, network: item.network, address: item.address, state: item.state };
|
|
25
|
+
});
|
|
26
|
+
if (wallets.filter((wallet) => wallet.kind === "wallet").length > maxWalletsPerKind || wallets.filter((wallet) => wallet.kind === "linked_wallet").length > maxWalletsPerKind) {
|
|
27
|
+
throw new ClientFlowError("invalid_server_response");
|
|
28
|
+
}
|
|
29
|
+
return { requestId: root.request_id, account: { accountId: account.account_id, version: account.version, setupRequired: account.setup_required }, sessionId: root.session_id, serverTime: root.server_time, wallets };
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type MediatedLinkedSendDisplay } from "./linkedSendDisplay.ts";
|
|
2
|
+
import type { UserWalletOperation } from "./walletOperations.ts";
|
|
3
|
+
export type { MediatedLinkedSendDisplay } from "./linkedSendDisplay.ts";
|
|
4
|
+
/** The display of a Mode C transfer from a wallet bound in the provider: the network coin only. */
|
|
5
|
+
export interface MediatedSendDisplay {
|
|
6
|
+
kind: "wallet.send";
|
|
7
|
+
ownerId: string;
|
|
8
|
+
requesterId: string;
|
|
9
|
+
walletId: string;
|
|
10
|
+
network: string;
|
|
11
|
+
asset: "native";
|
|
12
|
+
recipient: string;
|
|
13
|
+
amountRaw: string;
|
|
14
|
+
maxFeeRaw: string;
|
|
15
|
+
preparedSummary?: string;
|
|
16
|
+
}
|
|
17
|
+
/** A Mode C transfer past its material retention: its identity only; a linked wallet stays named by the operation's linkedWallet. */
|
|
18
|
+
export interface MediatedPurgedDisplay {
|
|
19
|
+
kind: "wallet.send";
|
|
20
|
+
materialPurged: true;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
requesterId: string;
|
|
23
|
+
walletId?: string;
|
|
24
|
+
}
|
|
25
|
+
export type MediatedDisplay = MediatedSendDisplay | MediatedLinkedSendDisplay | MediatedPurgedDisplay;
|
|
26
|
+
/** parseMediatedOperation reads exactly the mediated status of one Mode C send. */
|
|
27
|
+
export declare function parseMediatedOperation(payload: unknown): UserWalletOperation;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The status of a Mode C send as its owner reads it: a transfer a service
|
|
2
|
+
// forwarded on the person's signed request (authorization basis
|
|
3
|
+
// user_request_signed), from a wallet bound in the provider or from the
|
|
4
|
+
// person's linked wallet. The provider projects it as the mediated status on
|
|
5
|
+
// every route, the owner's GET of the operation and the account's send
|
|
6
|
+
// history included: it freezes no result recipient, and a linked transfer
|
|
7
|
+
// names its wallet by binding_source, connection_id, linked_wallet_id and
|
|
8
|
+
// binding_revision instead of wallet_id.
|
|
9
|
+
import { ClientFlowError, canonicalDecimal, deadlineAfter, isNextAction, opaqueId, record, rfc3339UTC } from "./auth.js";
|
|
10
|
+
import { bindingRevision, lowerUUID, rawQuantity, summaryOf, walletRecipient } from "./walletDisplayParts.js";
|
|
11
|
+
import { parseMediatedLinkedSendDisplay } from "./linkedSendDisplay.js";
|
|
12
|
+
const states = ["authorized", "dispatching", "outcome_unknown", "succeeded", "failed", "denied", "canceled", "expired"];
|
|
13
|
+
const ownNetwork = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
14
|
+
const digest = (v) => typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
|
|
15
|
+
const fault = () => { throw new ClientFlowError("invalid_server_response"); };
|
|
16
|
+
/** parseMediatedOperation reads exactly the mediated status of one Mode C send. */
|
|
17
|
+
export function parseMediatedOperation(payload) {
|
|
18
|
+
const r = record(payload, ["authorization_basis", "created_at", "display", "effect_state", "expires_at", "id", "kind", "next_action", "operation_fingerprint", "owner_id", "owner_type",
|
|
19
|
+
"request_fingerprint", "requester_id", "state", "version"], ["result_kind", "wallet_id", "binding_source", "connection_id", "linked_wallet_id", "binding_revision"]);
|
|
20
|
+
if (r.authorization_basis !== "user_request_signed" || r.kind !== "wallet.send" || r.owner_type !== "account" || !opaqueId(r.id) || !canonicalDecimal(r.version) ||
|
|
21
|
+
!opaqueId(r.owner_id) || !opaqueId(r.requester_id) || typeof r.state !== "string" || !states.includes(r.state) || !["no_effect", "possible", "known"].includes(r.effect_state) ||
|
|
22
|
+
!digest(r.request_fingerprint) || !digest(r.operation_fingerprint) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.expires_at) || !deadlineAfter(r.created_at, r.expires_at) ||
|
|
23
|
+
!isNextAction(r.next_action) || (r.result_kind !== undefined && r.result_kind !== "transfer_receipt"))
|
|
24
|
+
fault();
|
|
25
|
+
// A linked transfer is named by all of its linked identity and never beside a wallet_id.
|
|
26
|
+
const linked = r.binding_source !== undefined;
|
|
27
|
+
if (linked ? (r.binding_source !== "linked_wallet" || r.wallet_id !== undefined || !lowerUUID(r.connection_id) || !lowerUUID(r.linked_wallet_id) || !bindingRevision(r.binding_revision))
|
|
28
|
+
: (!opaqueId(r.wallet_id) || r.connection_id !== undefined || r.linked_wallet_id !== undefined || r.binding_revision !== undefined))
|
|
29
|
+
fault();
|
|
30
|
+
const display = mediatedDisplay(r.display, r, linked);
|
|
31
|
+
const operation = {
|
|
32
|
+
id: r.id, version: r.version, kind: "wallet.send", state: r.state, effectState: r.effect_state,
|
|
33
|
+
authorizationBasis: "user_request_signed", ownerType: "account", ownerId: r.owner_id, requesterId: r.requester_id,
|
|
34
|
+
requestFingerprint: r.request_fingerprint, operationFingerprint: r.operation_fingerprint, display,
|
|
35
|
+
createdAt: r.created_at, expiresAt: r.expires_at, nextAction: r.next_action,
|
|
36
|
+
};
|
|
37
|
+
if (linked)
|
|
38
|
+
operation.linkedWallet = { connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision };
|
|
39
|
+
else
|
|
40
|
+
operation.walletId = r.wallet_id;
|
|
41
|
+
if (r.result_kind !== undefined)
|
|
42
|
+
operation.resultKind = "transfer_receipt";
|
|
43
|
+
return operation;
|
|
44
|
+
}
|
|
45
|
+
function mediatedDisplay(value, r, linked) {
|
|
46
|
+
if (value !== null && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "material_purged")) {
|
|
47
|
+
const d = record(value, ["kind", "material_purged", "owner_id", "requester_id"], [linked ? "linked_wallet_id" : "wallet_id"]);
|
|
48
|
+
const own = linked ? d.linked_wallet_id === r.linked_wallet_id : d.wallet_id === r.wallet_id;
|
|
49
|
+
if (d.kind !== "wallet.send" || d.material_purged !== true || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id || !own)
|
|
50
|
+
fault();
|
|
51
|
+
const purged = { kind: "wallet.send", materialPurged: true, ownerId: d.owner_id, requesterId: d.requester_id };
|
|
52
|
+
if (!linked)
|
|
53
|
+
purged.walletId = d.wallet_id;
|
|
54
|
+
return purged;
|
|
55
|
+
}
|
|
56
|
+
if (linked) {
|
|
57
|
+
const d = parseMediatedLinkedSendDisplay(value);
|
|
58
|
+
if (d.ownerId !== r.owner_id || d.requesterId !== r.requester_id || d.linkedWalletId !== r.linked_wallet_id || d.connectionId !== r.connection_id || d.bindingRevision !== r.binding_revision)
|
|
59
|
+
fault();
|
|
60
|
+
return d;
|
|
61
|
+
}
|
|
62
|
+
const d = record(value, ["amount_raw", "asset", "kind", "max_fee_raw", "network", "owner_id", "recipient", "requester_id", "wallet_id"], ["prepared_summary"]);
|
|
63
|
+
if (d.kind !== "wallet.send" || d.asset !== "native" || !rawQuantity(d.amount_raw, true) || !rawQuantity(d.max_fee_raw, false) || typeof d.network !== "string" || !ownNetwork.test(d.network) ||
|
|
64
|
+
typeof d.recipient !== "string" || !walletRecipient.test(d.recipient) || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id || d.wallet_id !== r.wallet_id)
|
|
65
|
+
fault();
|
|
66
|
+
return { kind: "wallet.send", ownerId: d.owner_id, requesterId: d.requester_id, walletId: d.wallet_id, network: d.network, asset: "native",
|
|
67
|
+
recipient: d.recipient, amountRaw: d.amount_raw, maxFeeRaw: d.max_fee_raw, ...summaryOf(d.prepared_summary) };
|
|
68
|
+
}
|