@shuttle.technology/authorization 0.0.0-stage → 0.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/README.md +708 -2
- package/dist/aesGcmSiv.d.ts +14 -0
- package/dist/aesGcmSiv.js +160 -0
- package/dist/auth.d.ts +68 -0
- package/dist/auth.js +215 -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 +264 -0
- package/dist/client.js +754 -0
- package/dist/deviceKey.d.ts +13 -0
- package/dist/deviceKey.js +28 -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/linkedChecks.d.ts +17 -0
- package/dist/linkedChecks.js +31 -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 +373 -0
- package/dist/linkedWallets.js +471 -0
- package/dist/mediatedOperation.d.ts +27 -0
- package/dist/mediatedOperation.js +68 -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/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/slice4.d.ts +173 -0
- package/dist/slice4.js +285 -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/walletDisplayParts.d.ts +15 -0
- package/dist/walletDisplayParts.js +43 -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 +192 -0
- package/dist/walletOperations.js +308 -0
- package/package.json +64 -4
|
@@ -0,0 +1,471 @@
|
|
|
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, deadlineAfter, integer, isNextAction, opaqueId, record, rfc3339UTC, unexpiredRFC3339 } from './auth.js';
|
|
12
|
+
import { appendEventTypes, parseWalletEventPage, MaxStreamsActivityTypes, MaxStreamsCursorLength, MaxStreamsPageLimit } from './streams.js';
|
|
13
|
+
export { resumeCursor } from './streams.js';
|
|
14
|
+
import { parseOperationEnvelope } from './slice4.js';
|
|
15
|
+
import { linkedSignalsPath, readLinkedWalletSignals } from './linkedWalletSignals.js';
|
|
16
|
+
export { linkedSignalsPath, readLinkedWalletSignals, MaxSignalWallets } from './linkedWalletSignals.js';
|
|
17
|
+
import { parseTransferStatus, transferStatusPath } from './transferStatus.js';
|
|
18
|
+
import { parseLinkedWalletBalances } from './balances.js';
|
|
19
|
+
export { parseLinkedWalletBalances } from './balances.js';
|
|
20
|
+
import { parseSendPage, sendsPath } from './sends.js';
|
|
21
|
+
export { eventOfTransaction, parseSendPage, sendEvents, sendsPath } from './sends.js';
|
|
22
|
+
export { parseTransferStatus, transferFinal, transferStatusPath, TransferStates } from './transferStatus.js';
|
|
23
|
+
import { parseWalletLimits, walletLimitsCreate } from './walletLimits.js';
|
|
24
|
+
export { WalletLimitRefusals, isWalletLimitRefusal, MaxLimitCeilings, MaxLimitRecipients } from './walletLimits.js';
|
|
25
|
+
import { activityTypes, assets, b64, digest, fault, hex, https, network, publicKey, recipient, requestID, requestUUID, safeRevision, text, token, uuid } from './linkedChecks.js';
|
|
26
|
+
import { linkedSendAsset, parseUserWalletOperationEnvelope, replaceAction, transferComment } from './walletOperations.js';
|
|
27
|
+
export class LinkedWallets {
|
|
28
|
+
#transport;
|
|
29
|
+
constructor(transport) { this.#transport = transport; }
|
|
30
|
+
async challenge(cognitoAccessToken, credentialId, signal) {
|
|
31
|
+
token(cognitoAccessToken);
|
|
32
|
+
requestID(credentialId);
|
|
33
|
+
const { payload } = await this.#transport.write('/v1/wallet-connections/challenges', 'POST', { cognito_access_token: cognitoAccessToken, passkey_credential_id: credentialId }, signal);
|
|
34
|
+
const challenge = parseWalletLinkChallenge(payload, this.#transport.now());
|
|
35
|
+
if (challenge.publicKey.allowCredentials[0].id !== credentialId)
|
|
36
|
+
fault();
|
|
37
|
+
return challenge;
|
|
38
|
+
}
|
|
39
|
+
async connect(challengeId, cognitoAccessToken, assertion, signal) {
|
|
40
|
+
requestUUID(challengeId);
|
|
41
|
+
token(cognitoAccessToken);
|
|
42
|
+
validateWalletLinkAssertion(assertion);
|
|
43
|
+
const encoded = JSON.stringify(assertion);
|
|
44
|
+
if (new TextEncoder().encode(encoded).length > 65536)
|
|
45
|
+
throw new ClientFlowError('invalid_linked_wallet_request');
|
|
46
|
+
return parseWalletConnection((await this.#transport.write('/v1/wallet-connections', 'POST', { challenge_id: challengeId, cognito_access_token: cognitoAccessToken, response: encoded }, signal)).payload);
|
|
47
|
+
}
|
|
48
|
+
async connection(connectionId, signal) {
|
|
49
|
+
requestUUID(connectionId);
|
|
50
|
+
const connection = parseWalletConnection(await this.#transport.get(`/v1/wallet-connections/${connectionId}`, signal));
|
|
51
|
+
if (connection.id !== connectionId)
|
|
52
|
+
fault();
|
|
53
|
+
return connection;
|
|
54
|
+
}
|
|
55
|
+
async sync(connectionId, cognitoAccessToken, signal) {
|
|
56
|
+
requestUUID(connectionId);
|
|
57
|
+
token(cognitoAccessToken);
|
|
58
|
+
const connection = parseWalletConnection((await this.#transport.write(`/v1/wallet-connections/${connectionId}/sync`, 'POST', { cognito_access_token: cognitoAccessToken }, signal)).payload);
|
|
59
|
+
if (connection.id !== connectionId)
|
|
60
|
+
fault();
|
|
61
|
+
return connection;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* refresh binds the account to the Wallet identity the provider owns for it
|
|
65
|
+
* and synchronizes its public wallet inventory. It carries no Wallet token
|
|
66
|
+
* and no body; repeating it converges on the same connection. Call it after
|
|
67
|
+
* a wallet was created so the wallet appears in list().
|
|
68
|
+
*/
|
|
69
|
+
async refresh(signal) {
|
|
70
|
+
const connection = parseWalletConnection((await this.#transport.write('/v1/wallet-connections/brokered', 'POST', undefined, signal)).payload);
|
|
71
|
+
if (connection.state !== 'connected')
|
|
72
|
+
fault();
|
|
73
|
+
return connection;
|
|
74
|
+
}
|
|
75
|
+
async disconnect(connectionId, signal) {
|
|
76
|
+
requestUUID(connectionId);
|
|
77
|
+
const connection = parseWalletConnection((await this.#transport.write(`/v1/wallet-connections/${connectionId}`, 'DELETE', undefined, signal)).payload);
|
|
78
|
+
if (connection.id !== connectionId || connection.state !== 'revoked')
|
|
79
|
+
fault();
|
|
80
|
+
return connection;
|
|
81
|
+
}
|
|
82
|
+
async list(signal) {
|
|
83
|
+
return parseLinkedWalletList(await this.#transport.get('/v1/linked-wallets', signal));
|
|
84
|
+
}
|
|
85
|
+
async wallet(linkedWalletId, signal) {
|
|
86
|
+
requestUUID(linkedWalletId);
|
|
87
|
+
const root = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}`, signal), ['request_id', 'wallet']);
|
|
88
|
+
if (!opaqueId(root.request_id))
|
|
89
|
+
fault();
|
|
90
|
+
const wallet = parseLinkedWallet(root.wallet);
|
|
91
|
+
if (wallet.id !== linkedWalletId)
|
|
92
|
+
fault();
|
|
93
|
+
return { requestId: root.request_id, wallet };
|
|
94
|
+
}
|
|
95
|
+
/** Every metadata page is fresh; restart at the beginning to discover earlier insertions. */
|
|
96
|
+
async registrations(linkedWalletId, options = {}) {
|
|
97
|
+
const path = linkedRegistrationsPath(linkedWalletId, options);
|
|
98
|
+
const page = parseLinkedStreamsRegistrationList(await this.#transport.get(path, options.signal), linkedWalletId);
|
|
99
|
+
if (page.items.length > (options.limit ?? 50) || (options.afterRegistrationId !== undefined && page.items.some(item => item.registrationId <= options.afterRegistrationId)))
|
|
100
|
+
fault();
|
|
101
|
+
return page;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* One page of the linked wallet's retained events as wallet events 3.0:
|
|
105
|
+
* what came in, what went out and what else happened, each with kind,
|
|
106
|
+
* status and revision, those of the kinds eventTypes names. Continuations
|
|
107
|
+
* are sealed opaque cursors.
|
|
108
|
+
*/
|
|
109
|
+
async events(linkedWalletId, options = {}) {
|
|
110
|
+
return parseWalletEventPage(await this.#transport.get(linkedEventsPath(linkedWalletId, options), options.signal));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Opens one signal stream for these linked wallets and reads it to its end.
|
|
114
|
+
* onOpen says the stream is live: read every wallet once then. onWallets
|
|
115
|
+
* names wallets with new events: read them with events(). The promise
|
|
116
|
+
* resolves with the reason the stream ended; a stream lives no longer than
|
|
117
|
+
* an access token, so the caller opens the next one. It rejects with the
|
|
118
|
+
* server's refusal, with the abort reason, or when the connection fails;
|
|
119
|
+
* keep an ordinary timed read for those times.
|
|
120
|
+
*/
|
|
121
|
+
async signals(linkedWalletIds, handlers) {
|
|
122
|
+
const body = await this.#transport.stream(linkedSignalsPath(linkedWalletIds), handlers.signal);
|
|
123
|
+
return readLinkedWalletSignals(body, linkedWalletIds, handlers);
|
|
124
|
+
}
|
|
125
|
+
async createRegistration(create, idempotencyKey, signal) {
|
|
126
|
+
validateLinkedRegistrationCreate(create, this.#transport.accountId());
|
|
127
|
+
requestID(idempotencyKey);
|
|
128
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
129
|
+
const operation = parseLinkedRegistrationOperationEnvelope(payload).operation;
|
|
130
|
+
if (operation.ownerId !== accountId || operation.linkedWalletId !== create.linked_wallet_id)
|
|
131
|
+
fault();
|
|
132
|
+
return operation;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Starts tracking a linked wallet on one network. An EVM wallet has one
|
|
136
|
+
* address on every EVM network, so it is registered on each of them with its
|
|
137
|
+
* own call. activityTypes names the kinds of activity events asked for beside
|
|
138
|
+
* deposits; omitted or empty means deposits only, and Streams refuses a kind
|
|
139
|
+
* the network does not have. The operation comes back authorized;
|
|
140
|
+
* executeRegistration() with its version dispatches it.
|
|
141
|
+
*/
|
|
142
|
+
register(input, idempotencyKey, signal) {
|
|
143
|
+
record(input, ['linkedWalletId', 'network'], ['assets', 'activityTypes', 'resultRecipient']);
|
|
144
|
+
const create = { kind: 'streams.register', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: input.linkedWalletId, payload: { network: input.network } };
|
|
145
|
+
if (input.assets !== undefined)
|
|
146
|
+
create.payload.assets = input.assets;
|
|
147
|
+
// An empty selection means deposits only and is not sent; any other value is checked with the whole request.
|
|
148
|
+
if (input.activityTypes !== undefined && !(Array.isArray(input.activityTypes) && input.activityTypes.length === 0))
|
|
149
|
+
create.payload.activity_types = input.activityTypes;
|
|
150
|
+
if (input.resultRecipient !== undefined)
|
|
151
|
+
create.result_recipient = input.resultRecipient;
|
|
152
|
+
return this.createRegistration(create, idempotencyKey, signal);
|
|
153
|
+
}
|
|
154
|
+
async operation(operationId, signal) {
|
|
155
|
+
requestID(operationId);
|
|
156
|
+
const operation = parseLinkedRegistrationOperationEnvelope(await this.#transport.get(`/v1/operations/${operationId}`, signal)).operation;
|
|
157
|
+
if (operation.id !== operationId)
|
|
158
|
+
fault();
|
|
159
|
+
return operation;
|
|
160
|
+
}
|
|
161
|
+
async executeRegistration(operationId, version, idempotencyKey, signal) {
|
|
162
|
+
requestID(operationId);
|
|
163
|
+
requestID(idempotencyKey);
|
|
164
|
+
if (!canonicalDecimal(version))
|
|
165
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
166
|
+
const operation = parseLinkedRegistrationOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', undefined, signal, idempotencyKey, version)).payload).operation;
|
|
167
|
+
if (operation.id !== operationId)
|
|
168
|
+
fault();
|
|
169
|
+
return operation;
|
|
170
|
+
}
|
|
171
|
+
async registrationResult(operationId, signal) {
|
|
172
|
+
requestID(operationId);
|
|
173
|
+
return parseLinkedRegistrationResult(await this.#transport.get(`/v1/operations/${operationId}/result`, signal), operationId);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Starts a send from a linked wallet. The account's session authorizes it:
|
|
177
|
+
* the operation comes back authorized (authorizationBasis
|
|
178
|
+
* session_authorized), with no passkey and no confirmation window. Then
|
|
179
|
+
* prepare() has the own-wallet system prepare the transfer and name its
|
|
180
|
+
* fee; show the owner what was prepared and, on their confirm,
|
|
181
|
+
* executeSend() with the device share.
|
|
182
|
+
*/
|
|
183
|
+
async send(input, idempotencyKey, signal) {
|
|
184
|
+
const declared = record(input, ['linkedWalletId', 'network', 'asset', 'recipient', 'amountRaw'], ['comment', 'resultRecipient']);
|
|
185
|
+
requestID(idempotencyKey);
|
|
186
|
+
if (!uuid(declared.linkedWalletId) || !network(declared.network) || !linkedSendAsset(declared.asset) || typeof declared.recipient !== 'string' || !/^[\x21-\x7e]{1,256}$/.test(declared.recipient) ||
|
|
187
|
+
!canonicalDecimal(declared.amountRaw) || declared.amountRaw === '0' || declared.amountRaw.length > 78 ||
|
|
188
|
+
// Only a TON transfer carries a comment.
|
|
189
|
+
(declared.comment !== undefined && (!transferComment(declared.comment) || !declared.network.startsWith('ton:'))))
|
|
190
|
+
throw new ClientFlowError('invalid_operation_create');
|
|
191
|
+
const sent = { network: declared.network, asset: declared.asset, recipient: declared.recipient, amount_raw: declared.amountRaw };
|
|
192
|
+
if (declared.comment !== undefined)
|
|
193
|
+
sent.comment = declared.comment;
|
|
194
|
+
const create = { kind: 'wallet.send', owner_type: 'account', owner_id: this.#transport.accountId(), linked_wallet_id: declared.linkedWalletId, payload: sent };
|
|
195
|
+
if (declared.resultRecipient !== undefined)
|
|
196
|
+
create.result_recipient = recipient(declared.resultRecipient);
|
|
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'], ['resultRecipient']);
|
|
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
|
+
if (declared.resultRecipient !== undefined)
|
|
224
|
+
create.result_recipient = recipient(declared.resultRecipient);
|
|
225
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
226
|
+
const operation = parseUserWalletOperationEnvelope(payload).operation;
|
|
227
|
+
const shown = operation.display;
|
|
228
|
+
if (operation.kind !== 'wallet.send' || operation.ownerId !== accountId || operation.linkedWallet?.linkedWalletId !== declared.linkedWalletId ||
|
|
229
|
+
shown.replacesOperationId !== declared.operationId || shown.replaceAction !== declared.action)
|
|
230
|
+
fault();
|
|
231
|
+
return operation;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* What the wallet holds on each of its networks: the coin and the tokens
|
|
235
|
+
* the stand names there, in minimal units with their decimals. The provider
|
|
236
|
+
* reads its own stand's nodes. A network whose node gave no amount is in
|
|
237
|
+
* the answer with its status and without amounts; it fails nothing.
|
|
238
|
+
*/
|
|
239
|
+
async balances(linkedWalletId, signal) {
|
|
240
|
+
requestUUID(linkedWalletId);
|
|
241
|
+
return parseLinkedWalletBalances(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/balances`, signal), linkedWalletId);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Has the own-wallet system prepare the transfer of a send or replacement
|
|
245
|
+
* the session authorized, and returns the send with what was prepared:
|
|
246
|
+
* display.preparedSummary, the sending address and the fee estimate. Show
|
|
247
|
+
* them to the owner; executeSend() sends exactly that transfer. The
|
|
248
|
+
* transfer is prepared once: calling again returns the same prepared send
|
|
249
|
+
* and asks the own-wallet system nothing. A prepared send expires within
|
|
250
|
+
* 150 seconds; one that expired is sent again from send(). The request
|
|
251
|
+
* carries no body, idempotency key or version.
|
|
252
|
+
*/
|
|
253
|
+
async prepare(operationId, signal) {
|
|
254
|
+
requestID(operationId);
|
|
255
|
+
const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/prepare`, 'POST', undefined, signal)).payload).operation;
|
|
256
|
+
if (operation.id !== operationId || operation.kind !== 'wallet.send' || operation.linkedWallet === undefined)
|
|
257
|
+
fault();
|
|
258
|
+
return operation;
|
|
259
|
+
}
|
|
260
|
+
/** The key the device seals its share to, read right before executeSend(). */
|
|
261
|
+
async transportKey(linkedWalletId, signal) {
|
|
262
|
+
requestUUID(linkedWalletId);
|
|
263
|
+
const r = record(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/transport-key`, signal), ['request_id', 'public_key', 'encryption_algorithm']);
|
|
264
|
+
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))
|
|
265
|
+
fault();
|
|
266
|
+
return { requestId: r.request_id, publicKey: r.public_key, encryptionAlgorithm: r.encryption_algorithm };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Executes a prepared send, on the owner's confirm, with the device share
|
|
270
|
+
* resealed for the own-wallet system (resealDeviceShare). The provider
|
|
271
|
+
* cannot open the share. A send never prepared is refused with
|
|
272
|
+
* VERSION_CONFLICT, an expired one with OPERATION_EXPIRED.
|
|
273
|
+
*/
|
|
274
|
+
async executeSend(operationId, version, idempotencyKey, deviceShare, signal) {
|
|
275
|
+
requestID(operationId);
|
|
276
|
+
requestID(idempotencyKey);
|
|
277
|
+
const share = record(deviceShare, ['ciphertext', 'nonce', 'ephemeralPublicKey', 'encryptionAlgorithm', 'encryptionContext']);
|
|
278
|
+
if (!canonicalDecimal(version) || ![share.ciphertext, share.nonce, share.ephemeralPublicKey, share.encryptionContext].every(hex) || typeof share.encryptionAlgorithm !== 'string')
|
|
279
|
+
throw new ClientFlowError('invalid_execute_request');
|
|
280
|
+
const body = { device_share: { ciphertext: share.ciphertext, nonce: share.nonce, ephemeral_public_key: share.ephemeralPublicKey, encryption_algorithm: share.encryptionAlgorithm, encryption_context: share.encryptionContext } };
|
|
281
|
+
const operation = parseUserWalletOperationEnvelope((await this.#transport.write(`/v1/operations/${operationId}/execute`, 'POST', body, signal, idempotencyKey, version)).payload).operation;
|
|
282
|
+
if (operation.id !== operationId)
|
|
283
|
+
fault();
|
|
284
|
+
return operation;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* What became of the transfer a send prepared: on its way, confirmed by the
|
|
288
|
+
* network, or failed and why. The send's operation succeeds when the
|
|
289
|
+
* own-wallet system accepted the transfer; this is what the network did
|
|
290
|
+
* with it. Read it until final is true. Before the send was prepared
|
|
291
|
+
* there is no transfer and the read is refused with NOT_FOUND.
|
|
292
|
+
*/
|
|
293
|
+
async transferStatus(operationId, signal) {
|
|
294
|
+
return parseTransferStatus(await this.#transport.get(transferStatusPath(operationId), signal), operationId);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* One page of the account's send history, newest first: every wallet.send
|
|
298
|
+
* the account can read, with its transaction hash and the recorded final
|
|
299
|
+
* fate of its transfer. A send with a hash and no transfer has not been
|
|
300
|
+
* reported final yet: read transferStatus() for it, which records the fate.
|
|
301
|
+
* The cursor carries the first page's limit.
|
|
302
|
+
*/
|
|
303
|
+
async sends(options) {
|
|
304
|
+
const { path, signal } = sendsPath(options);
|
|
305
|
+
return parseSendPage(await this.#transport.get(path, signal));
|
|
306
|
+
}
|
|
307
|
+
/** The limits every send from the wallet obeys, read with the session (walletLimits.ts). Revision "0": never set. */
|
|
308
|
+
async limits(linkedWalletId, signal) {
|
|
309
|
+
requestUUID(linkedWalletId);
|
|
310
|
+
return parseWalletLimits(await this.#transport.get(`/v1/linked-wallets/${linkedWalletId}/limits`, signal), linkedWalletId);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Asks to replace the wallet's limits as a whole; empty ceilings and recipients remove them all. The operation
|
|
314
|
+
* awaits the owner's passkey: the client's challengeOperation(), completeOperationConfirmation() and
|
|
315
|
+
* executeOperation() apply it. A send the limits refuse answers one of WalletLimitRefusals.
|
|
316
|
+
*/
|
|
317
|
+
async setLimits(linkedWalletId, limits, idempotencyKey, signal) {
|
|
318
|
+
const create = walletLimitsCreate(this.#transport.accountId(), linkedWalletId, limits);
|
|
319
|
+
requestID(idempotencyKey);
|
|
320
|
+
const { payload, accountId } = await this.#transport.write('/v1/operations', 'POST', create, signal, idempotencyKey);
|
|
321
|
+
const operation = parseOperationEnvelope(payload).operation;
|
|
322
|
+
if (operation.display.kind !== 'wallet_limits.set' || operation.ownerId !== accountId || operation.display.linkedWalletId !== linkedWalletId)
|
|
323
|
+
fault();
|
|
324
|
+
return operation;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
export function parseWalletConnection(payload) {
|
|
328
|
+
const r = record(payload, ['request_id', 'id', 'connector_id', 'state', 'created_at', 'last_synced_at'], ['revoked_at']);
|
|
329
|
+
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)))
|
|
330
|
+
fault();
|
|
331
|
+
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 };
|
|
332
|
+
if (r.revoked_at !== undefined)
|
|
333
|
+
out.revokedAt = r.revoked_at;
|
|
334
|
+
return Object.freeze(out);
|
|
335
|
+
}
|
|
336
|
+
export function parseLinkedWallet(payload) {
|
|
337
|
+
const r = record(payload, ['id', 'connection_id', 'external_wallet_id', 'external_key_id', 'network', 'address', 'algorithm', 'public_key', 'binding_revision', 'status', 'observed_at']);
|
|
338
|
+
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))
|
|
339
|
+
fault();
|
|
340
|
+
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 });
|
|
341
|
+
}
|
|
342
|
+
export function parseLinkedWalletList(payload) {
|
|
343
|
+
const r = record(payload, ['request_id', 'items']);
|
|
344
|
+
if (!opaqueId(r.request_id) || !Array.isArray(r.items) || r.items.length > 1000)
|
|
345
|
+
fault();
|
|
346
|
+
const items = r.items.map(parseLinkedWallet);
|
|
347
|
+
if (items.some(w => w.status !== 'active') || new Set(items.map(w => w.id)).size !== items.length)
|
|
348
|
+
fault();
|
|
349
|
+
return { requestId: r.request_id, items: Object.freeze(items) };
|
|
350
|
+
}
|
|
351
|
+
export function parseWalletLinkChallenge(payload, now) {
|
|
352
|
+
const r = record(payload, ['request_id', 'challenge_id', 'expires_at', 'public_key']);
|
|
353
|
+
const p = record(r.public_key, ['allow_credentials', 'challenge', 'rp_id', 'timeout_ms', 'user_verification']);
|
|
354
|
+
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)
|
|
355
|
+
fault();
|
|
356
|
+
const c = record(p.allow_credentials[0], ['id', 'type']);
|
|
357
|
+
if (c.type !== 'public-key' || !b64(c.id))
|
|
358
|
+
fault();
|
|
359
|
+
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' }] } };
|
|
360
|
+
}
|
|
361
|
+
export function validateWalletLinkAssertion(value) {
|
|
362
|
+
const r = record(value, ['id', 'rawId', 'type', 'response']);
|
|
363
|
+
const p = record(r.response, ['clientDataJSON', 'authenticatorData', 'signature', 'userHandle']);
|
|
364
|
+
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)))
|
|
365
|
+
throw new ClientFlowError('invalid_linked_wallet_request');
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
export function validateLinkedRegistrationCreate(value, ownerId) {
|
|
369
|
+
const r = record(value, ['kind', 'owner_type', 'owner_id', 'linked_wallet_id', 'payload'], ['result_recipient']);
|
|
370
|
+
const p = record(r.payload, ['network'], ['assets', 'activity_types']);
|
|
371
|
+
if (r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || r.owner_id !== ownerId || !uuid(r.linked_wallet_id) || !network(p.network) || (p.assets !== undefined && !assets(p.assets)) || (p.activity_types !== undefined && !activityTypes(p.activity_types)))
|
|
372
|
+
throw new ClientFlowError('invalid_operation_create');
|
|
373
|
+
if (r.result_recipient !== undefined)
|
|
374
|
+
recipient(r.result_recipient);
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
export function parseLinkedRegistrationOperationEnvelope(payload) {
|
|
378
|
+
const r = record(payload, ['request_id', 'operation']);
|
|
379
|
+
if (!opaqueId(r.request_id))
|
|
380
|
+
fault();
|
|
381
|
+
return { requestId: r.request_id, operation: parseLinkedRegistrationOperation(r.operation) };
|
|
382
|
+
}
|
|
383
|
+
export function parseLinkedRegistrationOperation(payload) {
|
|
384
|
+
const r = record(payload, ['id', 'version', 'kind', 'state', 'effect_state', 'authorization_basis', 'owner_type', 'owner_id', 'requester_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'request_fingerprint', 'operation_fingerprint', 'display', 'created_at', 'expires_at', 'next_action'], ['confirmation_location']);
|
|
385
|
+
if (!opaqueId(r.id) || !canonicalDecimal(r.version) || r.kind !== 'streams.register' || r.owner_type !== 'account' || !opaqueId(r.owner_id) || !opaqueId(r.requester_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || !safeRevision(r.binding_revision) || !digest(r.request_fingerprint) || !digest(r.operation_fingerprint) || !rfc3339UTC(r.created_at) || !rfc3339UTC(r.expires_at) || !deadlineAfter(r.created_at, r.expires_at) || !isNextAction(r.next_action) || !['awaiting_confirmation', 'authorized', 'dispatching', 'outcome_unknown', 'succeeded', 'failed', 'denied', 'canceled', 'expired'].includes(r.state) || !['no_effect', 'possible', 'known'].includes(r.effect_state) || !['', 'user_confirmed', 'session_authorized'].includes(r.authorization_basis) || (r.confirmation_location !== undefined && !https(r.confirmation_location)))
|
|
386
|
+
fault();
|
|
387
|
+
const purged = typeof r.display === 'object' && r.display !== null && 'material_purged' in r.display;
|
|
388
|
+
const d = record(r.display, ['kind', 'owner_id', 'requester_id', 'result_recipient', ...(purged ? ['material_purged'] : ['binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'assets', 'network', 'address', 'streams_mapping_id', 'streams_mapping_version', 'target_customer_id', 'target_project_id', 'target_environment_id'])]);
|
|
389
|
+
if (d.kind !== r.kind || d.owner_id !== r.owner_id || d.requester_id !== r.requester_id)
|
|
390
|
+
fault();
|
|
391
|
+
const common = { kind: 'streams.register', ownerId: r.owner_id, requesterId: r.requester_id, resultRecipient: recipient(d.result_recipient) };
|
|
392
|
+
let display;
|
|
393
|
+
if (purged) {
|
|
394
|
+
if (d.material_purged !== true)
|
|
395
|
+
fault();
|
|
396
|
+
display = { ...common, materialPurged: true };
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
if (d.binding_source !== r.binding_source || d.connection_id !== r.connection_id || d.linked_wallet_id !== r.linked_wallet_id || d.binding_revision !== r.binding_revision || !assets(d.assets) || !network(d.network) || !text(d.address, 256) || !safeRevision(d.streams_mapping_version) || ![d.streams_mapping_id, d.target_customer_id, d.target_project_id, d.target_environment_id].every(opaqueId))
|
|
400
|
+
fault();
|
|
401
|
+
display = { ...common, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, assets: Object.freeze([...d.assets]), network: d.network, address: d.address, streamsMappingId: d.streams_mapping_id, streamsMappingVersion: d.streams_mapping_version, targetCustomerId: d.target_customer_id, targetProjectId: d.target_project_id, targetEnvironmentId: d.target_environment_id };
|
|
402
|
+
}
|
|
403
|
+
const operation = { id: r.id, version: r.version, kind: 'streams.register', state: r.state, effectState: r.effect_state, authorizationBasis: r.authorization_basis, ownerType: 'account', ownerId: r.owner_id, requesterId: r.requester_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, requestFingerprint: r.request_fingerprint, operationFingerprint: r.operation_fingerprint, display, createdAt: r.created_at, expiresAt: r.expires_at, nextAction: r.next_action };
|
|
404
|
+
if (r.confirmation_location !== undefined)
|
|
405
|
+
operation.confirmationLocation = r.confirmation_location;
|
|
406
|
+
return operation;
|
|
407
|
+
}
|
|
408
|
+
export function parseLinkedRegistrationResult(payload, operationId) {
|
|
409
|
+
const r = record(payload, ['request_id', 'result']);
|
|
410
|
+
const v = record(r.result, ['kind', 'operation_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'registration_digest', 'receipt_id']);
|
|
411
|
+
if (!opaqueId(r.request_id) || !opaqueId(v.operation_id) || v.operation_id !== operationId || v.kind !== 'streams_registration' || v.binding_source !== 'linked_wallet' || !uuid(v.connection_id) || !uuid(v.linked_wallet_id) || !safeRevision(v.binding_revision) || !text(v.source_registration_id, 128) || !text(v.receipt_id, 128) || !digest(v.registration_digest))
|
|
412
|
+
fault();
|
|
413
|
+
return { requestId: r.request_id, result: { kind: 'streams_registration', operationId: v.operation_id, bindingSource: 'linked_wallet', connectionId: v.connection_id, linkedWalletId: v.linked_wallet_id, bindingRevision: v.binding_revision, sourceRegistrationId: v.source_registration_id, registrationDigest: v.registration_digest, receiptId: v.receipt_id } };
|
|
414
|
+
}
|
|
415
|
+
export function linkedRegistrationsPath(linkedWalletId, options = {}) {
|
|
416
|
+
requestUUID(linkedWalletId);
|
|
417
|
+
const declared = record(options, [], ['afterRegistrationId', 'limit', 'signal']);
|
|
418
|
+
const query = new URLSearchParams();
|
|
419
|
+
if (declared.afterRegistrationId !== undefined) {
|
|
420
|
+
requestUUID(declared.afterRegistrationId);
|
|
421
|
+
query.set('after_registration_id', declared.afterRegistrationId);
|
|
422
|
+
}
|
|
423
|
+
readLimit(query, declared.limit);
|
|
424
|
+
return linkedReadPath(linkedWalletId, 'registrations', query);
|
|
425
|
+
}
|
|
426
|
+
export function linkedEventsPath(linkedWalletId, options = {}) {
|
|
427
|
+
requestUUID(linkedWalletId);
|
|
428
|
+
const declared = record(options, [], ['registrationId', 'cursor', 'limit', 'eventTypes', 'signal']);
|
|
429
|
+
const query = new URLSearchParams();
|
|
430
|
+
if (declared.registrationId !== undefined) {
|
|
431
|
+
requestUUID(declared.registrationId);
|
|
432
|
+
query.set('registration_id', declared.registrationId);
|
|
433
|
+
}
|
|
434
|
+
if (declared.cursor !== undefined) {
|
|
435
|
+
if (typeof declared.cursor !== 'string' || declared.cursor.length < 1 || declared.cursor.length > MaxStreamsCursorLength)
|
|
436
|
+
throw new ClientFlowError('invalid_streams_request');
|
|
437
|
+
query.set('cursor', declared.cursor);
|
|
438
|
+
}
|
|
439
|
+
readLimit(query, declared.limit);
|
|
440
|
+
appendEventTypes(query, declared.eventTypes);
|
|
441
|
+
return linkedReadPath(linkedWalletId, 'events', query);
|
|
442
|
+
}
|
|
443
|
+
export function parseLinkedStreamsRegistrationList(payload, linkedWalletId) {
|
|
444
|
+
const root = record(payload, ['items', 'has_more'], ['next_after_registration_id']);
|
|
445
|
+
if (!Array.isArray(root.items) || root.items.length > MaxStreamsPageLimit || typeof root.has_more !== 'boolean' || root.has_more !== Object.hasOwn(root, 'next_after_registration_id'))
|
|
446
|
+
fault();
|
|
447
|
+
const items = root.items.map(item => parseLinkedStreamsRegistration(item, linkedWalletId));
|
|
448
|
+
if (items.some((item, index) => index > 0 && items[index - 1].registrationId >= item.registrationId))
|
|
449
|
+
fault();
|
|
450
|
+
if (root.has_more && (!uuid(root.next_after_registration_id) || items.length === 0 || root.next_after_registration_id !== items.at(-1).registrationId))
|
|
451
|
+
fault();
|
|
452
|
+
const page = { items: Object.freeze(items), hasMore: root.has_more };
|
|
453
|
+
return Object.freeze(root.has_more ? { ...page, nextAfterRegistrationId: root.next_after_registration_id } : page);
|
|
454
|
+
}
|
|
455
|
+
function parseLinkedStreamsRegistration(payload, linkedWalletId) {
|
|
456
|
+
const r = record(payload, ['registration_id', 'binding_source', 'connection_id', 'linked_wallet_id', 'binding_revision', 'source_registration_id', 'network', 'address', 'assets', 'status', 'effective_from', 'revision', 'source_version']);
|
|
457
|
+
if (!uuid(r.registration_id) || r.binding_source !== 'linked_wallet' || !uuid(r.connection_id) || !uuid(r.linked_wallet_id) || r.linked_wallet_id !== linkedWalletId || !safeRevision(r.binding_revision) || !opaqueId(r.source_registration_id) || !network(r.network) || typeof r.address !== 'string' || !/^[A-Za-z0-9_:-]{1,256}$/.test(r.address) || !assets(r.assets) || !['pending', 'active'].includes(r.status) || typeof r.effective_from !== 'string' || new TextEncoder().encode(r.effective_from).length < 1 || new TextEncoder().encode(r.effective_from).length > 256 || !integer(r.revision) || r.revision < 1 || typeof r.source_version !== 'string' || !/^[0-9]+\.[0-9]+$/.test(r.source_version))
|
|
458
|
+
fault();
|
|
459
|
+
return Object.freeze({ registrationId: r.registration_id, bindingSource: 'linked_wallet', connectionId: r.connection_id, linkedWalletId: r.linked_wallet_id, bindingRevision: r.binding_revision, sourceRegistrationId: r.source_registration_id, network: r.network, address: r.address, assets: Object.freeze([...r.assets]), status: r.status, effectiveFrom: r.effective_from, revision: r.revision, sourceVersion: r.source_version });
|
|
460
|
+
}
|
|
461
|
+
function linkedReadPath(id, kind, query) {
|
|
462
|
+
const path = `/v1/linked-wallets/${id}/${kind}`;
|
|
463
|
+
return query.size === 0 ? path : `${path}?${query.toString()}`;
|
|
464
|
+
}
|
|
465
|
+
function readLimit(query, value) {
|
|
466
|
+
if (value === undefined)
|
|
467
|
+
return;
|
|
468
|
+
if (!integer(value) || value < 1 || value > MaxStreamsPageLimit)
|
|
469
|
+
throw new ClientFlowError('invalid_streams_request');
|
|
470
|
+
query.set('limit', String(value));
|
|
471
|
+
}
|
|
@@ -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
|
+
}
|