@profullstack/coinpay 0.8.0 → 0.9.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 +39 -0
- package/README.md +128 -3
- package/bin/coinpay.js +919 -30
- package/package.json +24 -9
- package/src/card-payments.js +23 -14
- package/src/client.js +40 -2
- package/src/finances-tui.js +1005 -0
- package/src/finances.d.ts +333 -0
- package/src/finances.js +445 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +35 -0
- package/src/invoices.d.ts +1 -0
- package/src/invoices.js +14 -1
- package/src/lightning.js +8 -3
- package/src/swap.js +36 -5
- package/src/taskmarket.js +329 -0
- package/src/wallet.js +54 -24
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskMarket delegation for CoinPay agent wallets.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the CoinPayPortal x402 v2 rails to TaskMarket (https://taskmarket.dev):
|
|
5
|
+
* an agent or user can discover open work, fetch live task status, list
|
|
6
|
+
* submissions, and create a fully-funded task through the TaskMarket HTTP API
|
|
7
|
+
* using the same EIP-712 transfer-authorization mechanics as the rest of this
|
|
8
|
+
* SDK.
|
|
9
|
+
*
|
|
10
|
+
* Safety properties (mirroring the TaskMarket integration bounty rules):
|
|
11
|
+
*
|
|
12
|
+
* - No private key, seed phrase, token or cookie is ever requested, stored,
|
|
13
|
+
* logged or committed. All signing is delegated to a caller-supplied
|
|
14
|
+
* `signer` object (e.g. an EIP-1193 wallet or a CoinPay web-wallet signer).
|
|
15
|
+
* - Every spend requires fresh, explicit user authorization: the `authorize`
|
|
16
|
+
* callback is invoked with the exact amount, asset, payee and deadline, and
|
|
17
|
+
* must return `true` before any payment header is produced.
|
|
18
|
+
* - `spendingLimitUsd` caps the amount that may be authorized in one call.
|
|
19
|
+
* The guard is only enforced for assets with known decimals; for any other
|
|
20
|
+
* asset the call refuses to proceed rather than pay blind.
|
|
21
|
+
* - The created task's live status and its submissions are readable through
|
|
22
|
+
* `getTask` / `listSubmissions` and are meant to be presented to a human for
|
|
23
|
+
* review; this module never silently accepts or rejects work.
|
|
24
|
+
* - No blind payment retries: a second POST happens exactly once; if its
|
|
25
|
+
* settlement status is unknown the error tells the caller to verify with
|
|
26
|
+
* `getTask` instead of re-paying.
|
|
27
|
+
*
|
|
28
|
+
* @module taskmarket
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import {
|
|
33
|
+
buildAuthorization,
|
|
34
|
+
encodePaymentHeader,
|
|
35
|
+
requiredAmount,
|
|
36
|
+
selectAcceptEntry,
|
|
37
|
+
X402_VERSION,
|
|
38
|
+
} from './x402-v2.js';
|
|
39
|
+
|
|
40
|
+
/** TaskMarket API base URL (overridable for tests / self-hosted). */
|
|
41
|
+
export const TASKMARKET_API_URL =
|
|
42
|
+
process.env.TASKMARKET_API_URL ?? 'https://api.taskmarket.dev';
|
|
43
|
+
|
|
44
|
+
/** Amounts are quoted in base units; USDC settles at 1e6 per dollar. */
|
|
45
|
+
const DECIMALS = {
|
|
46
|
+
USDC: 6,
|
|
47
|
+
USDC_E: 6,
|
|
48
|
+
USDT: 6,
|
|
49
|
+
ETH: 18,
|
|
50
|
+
POL: 18,
|
|
51
|
+
SOL: 9,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Error raised for non-2xx responses from the TaskMarket API. */
|
|
55
|
+
export class TaskMarketError extends Error {
|
|
56
|
+
constructor(status, message, body) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = 'TaskMarketError';
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.body = body;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Error raised when the caller declines the payment authorization. */
|
|
65
|
+
export class PaymentNotAuthorizedError extends Error {
|
|
66
|
+
constructor(details) {
|
|
67
|
+
super('Payment not authorized by caller');
|
|
68
|
+
this.name = 'PaymentNotAuthorizedError';
|
|
69
|
+
this.details = details;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when a single 402 `accepts` entry stays within the caller's spending
|
|
75
|
+
* cap. Amount comparison happens in base units (`amount * 10^decimals`).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} accept one entry of the 402 `accepts` array
|
|
78
|
+
* @param {number} spendingLimitUsd maximum USD the caller allows
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function withinSpendingLimit(accept, spendingLimitUsd) {
|
|
82
|
+
if (!spendingLimitUsd) return true;
|
|
83
|
+
const asset = String(accept?.asset ?? '').toUpperCase();
|
|
84
|
+
const decimals = DECIMALS[asset];
|
|
85
|
+
if (decimals === undefined) {
|
|
86
|
+
throw new TaskMarketError(
|
|
87
|
+
0,
|
|
88
|
+
`Cannot enforce a USD spending limit for asset "${asset}": ` +
|
|
89
|
+
'its decimals are unknown. Pass an amount with known decimals or ' +
|
|
90
|
+
'leave spendingLimitUsd unset only if you fully trust the flow.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const amount = Number(requiredAmount(accept));
|
|
94
|
+
return amount <= spendingLimitUsd * 10 ** decimals;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse a TaskMarket JSON response, tolerating both `{data: {...}}` wrappers
|
|
99
|
+
* (task list) and bare payloads (task detail, submissions list).
|
|
100
|
+
*/
|
|
101
|
+
function unwrap(result, key) {
|
|
102
|
+
if (result && typeof result === 'object') {
|
|
103
|
+
if (key && result[key] !== undefined) return result[key];
|
|
104
|
+
if (result.data !== undefined) {
|
|
105
|
+
if (key && result.data[key] !== undefined) return result.data[key];
|
|
106
|
+
return result.data;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Discover open TaskMarket tasks.
|
|
114
|
+
*
|
|
115
|
+
* Listing is public on TaskMarket; a `signer` is therefore optional and only
|
|
116
|
+
* used to attach the EIP-191 read-auth headers when provided.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} [options]
|
|
119
|
+
* @param {string} [options.status='open']
|
|
120
|
+
* @param {number} [options.limit=50]
|
|
121
|
+
* @param {string} [options.phase] lifecycle filter (active, in_review, ...)
|
|
122
|
+
* @param {string} [options.mode] bounty | claim | pitch | benchmark | auction
|
|
123
|
+
* @param {string} [options.tags] comma-separated tags
|
|
124
|
+
* @param {object} [options.signer] optional `{address, signMessage}` wallet
|
|
125
|
+
* @param {Function} [options.fetchImpl] fetch substitute for tests
|
|
126
|
+
* @returns {Promise<object[]>}
|
|
127
|
+
*/
|
|
128
|
+
export async function discoverTasks(options = {}) {
|
|
129
|
+
const {
|
|
130
|
+
status = 'open',
|
|
131
|
+
limit = 50,
|
|
132
|
+
phase,
|
|
133
|
+
mode,
|
|
134
|
+
tags,
|
|
135
|
+
signer,
|
|
136
|
+
fetchImpl = fetch,
|
|
137
|
+
} = options;
|
|
138
|
+
const params = new URLSearchParams({ status: String(status), limit: String(limit) });
|
|
139
|
+
if (phase) params.set('phase', phase);
|
|
140
|
+
if (mode) params.set('mode', mode);
|
|
141
|
+
if (tags) params.set('tags', tags);
|
|
142
|
+
|
|
143
|
+
let headers = { 'User-Agent': 'coinpay-sdk-taskmarket/0.8' };
|
|
144
|
+
if (signer && signer.address && signer.signMessage) {
|
|
145
|
+
const message = `TaskMarket read auth for ${signer.address}`;
|
|
146
|
+
const signature = await signer.signMessage({ message });
|
|
147
|
+
headers['X-Taskmarket-Address'] = signer.address;
|
|
148
|
+
headers['X-Taskmarket-Signature'] = signature;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const res = await fetchImpl(`${TASKMARKET_API_URL}/api/tasks?${params}`, { headers });
|
|
152
|
+
const result = await res.json().catch(() => ({}));
|
|
153
|
+
if (!res.ok) throw new TaskMarketError(res.status, `GET /api/tasks failed (${res.status})`, result);
|
|
154
|
+
return unwrap(result, 'tasks') ?? [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fetch the live status of one TaskMarket task.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} taskId
|
|
161
|
+
* @param {object} [options]
|
|
162
|
+
* @param {Function} [options.fetchImpl]
|
|
163
|
+
* @returns {Promise<object>}
|
|
164
|
+
*/
|
|
165
|
+
export async function getTask(taskId, options = {}) {
|
|
166
|
+
const { fetchImpl = fetch } = options;
|
|
167
|
+
const res = await fetchImpl(`${TASKMARKET_API_URL}/api/tasks/${encodeURIComponent(taskId)}`, {
|
|
168
|
+
headers: { 'User-Agent': 'coinpay-sdk-taskmarket/0.8' },
|
|
169
|
+
});
|
|
170
|
+
const result = await res.json().catch(() => ({}));
|
|
171
|
+
if (!res.ok) throw new TaskMarketError(res.status, `GET /api/tasks/${taskId} failed (${res.status})`, result);
|
|
172
|
+
return unwrap(result);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* List the submissions of a TaskMarket task for human review.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} taskId
|
|
179
|
+
* @param {object} [options]
|
|
180
|
+
* @param {Function} [options.fetchImpl]
|
|
181
|
+
* @returns {Promise<object[]>}
|
|
182
|
+
*/
|
|
183
|
+
export async function listSubmissions(taskId, options = {}) {
|
|
184
|
+
const { fetchImpl = fetch } = options;
|
|
185
|
+
const res = await fetchImpl(
|
|
186
|
+
`${TASKMARKET_API_URL}/api/tasks/${encodeURIComponent(taskId)}/submissions`,
|
|
187
|
+
{ headers: { 'User-Agent': 'coinpay-sdk-taskmarket/0.8' } }
|
|
188
|
+
);
|
|
189
|
+
const result = await res.json().catch(() => []);
|
|
190
|
+
if (!res.ok) throw new TaskMarketError(res.status, `GET /api/tasks/${taskId}/submissions failed (${res.status})`, result);
|
|
191
|
+
return Array.isArray(result) ? result : (unwrap(result, 'submissions') ?? []);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create a TaskMarket task and pay for it over x402 v2.
|
|
196
|
+
*
|
|
197
|
+
* Flow:
|
|
198
|
+
* 1. POST the task body with an idempotency key.
|
|
199
|
+
* 2. TaskMarket answers `402 Payment Required` with the accepted rails.
|
|
200
|
+
* 3. `spendingLimitUsd` is enforced against the quoted amount.
|
|
201
|
+
* 4. `authorize` is invoked with the exact resource, payee, amount, asset
|
|
202
|
+
* and timeout; it must return `true` (fresh, explicit user consent).
|
|
203
|
+
* 5. The EIP-712 transfer authorization is built, signed by `signer`, and
|
|
204
|
+
* sent once as the `PAYMENT-SIGNATURE` header.
|
|
205
|
+
* 6. The created task id is returned; callers can then poll `getTask`.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} task `{title, description, reward, ...}` — fields are passed
|
|
208
|
+
* through verbatim (reward is in base units).
|
|
209
|
+
* @param {object} options
|
|
210
|
+
* @param {object} options.signer `{address, signTypedData({domain, types, primaryType, message})}`
|
|
211
|
+
* @param {string[]} [options.capabilities] CAIP-2 ids the signer can pay
|
|
212
|
+
* @param {number} [options.spendingLimitUsd] hard cap in USD for this call
|
|
213
|
+
* @param {Function} [options.authorize] `({resource, scheme, network, amount, asset, payTo, maxTimeoutSeconds}) => boolean`
|
|
214
|
+
* @param {string} [options.idempotencyKey] defaults to a fresh UUID
|
|
215
|
+
* @param {Function} [options.fetchImpl] fetch substitute for tests
|
|
216
|
+
* @returns {Promise<{taskId: string}>}
|
|
217
|
+
*/
|
|
218
|
+
export async function createTask(task, options) {
|
|
219
|
+
if (!options?.signer) throw new TypeError('createTask requires options.signer');
|
|
220
|
+
if (!task?.title || !task?.description) throw new TypeError('task requires title and description');
|
|
221
|
+
if (!(Number(task.reward) > 0)) throw new TypeError('task requires reward > 0 (base units)');
|
|
222
|
+
|
|
223
|
+
const {
|
|
224
|
+
signer,
|
|
225
|
+
capabilities = ['eip155:8453', 'eip155:1'],
|
|
226
|
+
spendingLimitUsd,
|
|
227
|
+
authorize,
|
|
228
|
+
idempotencyKey = randomUUID(),
|
|
229
|
+
fetchImpl = fetch,
|
|
230
|
+
} = options;
|
|
231
|
+
|
|
232
|
+
const url = `${TASKMARKET_API_URL}/api/tasks`;
|
|
233
|
+
const body = JSON.stringify(task);
|
|
234
|
+
|
|
235
|
+
const res = await fetchImpl(url, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
'User-Agent': 'coinpay-sdk-taskmarket/0.8',
|
|
240
|
+
'X-Taskmarket-Idempotency-Key': idempotencyKey,
|
|
241
|
+
},
|
|
242
|
+
body,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (res.ok) return res.json().catch(() => ({}));
|
|
246
|
+
if (res.status !== 402) {
|
|
247
|
+
const result = await res.json().catch(() => ({}));
|
|
248
|
+
throw new TaskMarketError(res.status, `POST /api/tasks failed (${res.status})`, result);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Payment required: pick a rail we can sign, then ask the user. ──
|
|
252
|
+
const requirements = await res.json().catch(() => ({}));
|
|
253
|
+
const accepts = Array.isArray(requirements.accepts) ? requirements.accepts : [];
|
|
254
|
+
if (accepts.length === 0) {
|
|
255
|
+
throw new TaskMarketError(402, 'TaskMarket sent no accepted payment methods', requirements);
|
|
256
|
+
}
|
|
257
|
+
const accept = selectAcceptEntry(accepts, capabilities) ?? accepts[0];
|
|
258
|
+
if (!accept) throw new TaskMarketError(402, 'No acceptable payment method for this wallet', requirements);
|
|
259
|
+
|
|
260
|
+
if (!withinSpendingLimit(accept, spendingLimitUsd)) {
|
|
261
|
+
throw new TaskMarketError(
|
|
262
|
+
402,
|
|
263
|
+
`Payment of ${requiredAmount(accept)} ${accept.asset} exceeds spendingLimitUsd=${spendingLimitUsd}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const details = {
|
|
268
|
+
resource: requirements.resource,
|
|
269
|
+
scheme: accept.scheme,
|
|
270
|
+
network: accept.network,
|
|
271
|
+
amount: requiredAmount(accept),
|
|
272
|
+
asset: accept.asset,
|
|
273
|
+
payTo: accept.payTo,
|
|
274
|
+
maxTimeoutSeconds: accept.maxTimeoutSeconds,
|
|
275
|
+
};
|
|
276
|
+
if (authorize && !(await authorize(details))) {
|
|
277
|
+
throw new PaymentNotAuthorizedError(details);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const authorization = buildAuthorization({
|
|
281
|
+
from: signer.address,
|
|
282
|
+
to: accept.payTo,
|
|
283
|
+
value: requiredAmount(accept),
|
|
284
|
+
validForSeconds: Number(accept.maxTimeoutSeconds ?? 300),
|
|
285
|
+
});
|
|
286
|
+
const signature = await signer.signTypedData({
|
|
287
|
+
domain: accept.extra?.eip712?.domain,
|
|
288
|
+
types: accept.extra?.eip712?.types,
|
|
289
|
+
primaryType: accept.extra?.eip712?.primaryType,
|
|
290
|
+
message: authorization,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const paymentPayload = {
|
|
294
|
+
x402Version: X402_VERSION,
|
|
295
|
+
resource: requirements.resource,
|
|
296
|
+
accepted: {
|
|
297
|
+
scheme: accept.scheme,
|
|
298
|
+
network: accept.network,
|
|
299
|
+
amount: requiredAmount(accept),
|
|
300
|
+
asset: accept.asset,
|
|
301
|
+
payTo: accept.payTo,
|
|
302
|
+
maxTimeoutSeconds: accept.maxTimeoutSeconds,
|
|
303
|
+
extra: accept.extra,
|
|
304
|
+
},
|
|
305
|
+
payload: { authorization, signature },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const paid = await fetchImpl(url, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: {
|
|
311
|
+
'Content-Type': 'application/json',
|
|
312
|
+
'User-Agent': 'coinpay-sdk-taskmarket/0.8',
|
|
313
|
+
'X-Taskmarket-Idempotency-Key': idempotencyKey,
|
|
314
|
+
'PAYMENT-SIGNATURE': encodePaymentHeader(paymentPayload),
|
|
315
|
+
},
|
|
316
|
+
body,
|
|
317
|
+
});
|
|
318
|
+
const result = await paid.json().catch(() => ({}));
|
|
319
|
+
if (!paid.ok) {
|
|
320
|
+
// Settlement state is unknown — never auto-retry the payment.
|
|
321
|
+
throw new TaskMarketError(
|
|
322
|
+
paid.status,
|
|
323
|
+
`POST /api/tasks failed after payment (${paid.status}). ` +
|
|
324
|
+
'Verify settlement with getTask(taskId) before retrying.',
|
|
325
|
+
result
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
package/src/wallet.js
CHANGED
|
@@ -407,8 +407,17 @@ function getSecp256k1PublicKey(seed) {
|
|
|
407
407
|
* @private
|
|
408
408
|
*/
|
|
409
409
|
function signMessage(message, privateKey) {
|
|
410
|
-
|
|
411
|
-
|
|
410
|
+
// Signed over the RAW message bytes, not sha256(message).
|
|
411
|
+
//
|
|
412
|
+
// This used to hash first. The server verifies proofs by passing the raw
|
|
413
|
+
// encoded message straight to `secp256k1.verify` (see
|
|
414
|
+
// web-wallet/auth.ts::verifySecp256k1Signature), and noble treats that
|
|
415
|
+
// argument as the thing that was signed — so a signature over the digest
|
|
416
|
+
// verifies false against the message. Every proof of ownership this SDK
|
|
417
|
+
// produced was rejected, which meant `WalletClient.fromSeed()` could not
|
|
418
|
+
// import a wallet at all. Found while wiring the same proof into
|
|
419
|
+
// `create()` for V-04.
|
|
420
|
+
const signature = secp256k1.sign(new TextEncoder().encode(message), privateKey);
|
|
412
421
|
// Handle different noble-curves versions:
|
|
413
422
|
// v1.x returns Signature object with toCompactHex()
|
|
414
423
|
// v2.x returns raw Uint8Array directly
|
|
@@ -672,11 +681,24 @@ export class WalletClient {
|
|
|
672
681
|
};
|
|
673
682
|
});
|
|
674
683
|
|
|
684
|
+
// V-04: /create accepted a public key and a list of on-chain addresses
|
|
685
|
+
// from anyone, with no proof that either was theirs, while /import required
|
|
686
|
+
// a signature for the same write. `wallet_addresses` is globally unique on
|
|
687
|
+
// (address, chain), so an unproved registration permanently denies an
|
|
688
|
+
// address to whoever actually owns it.
|
|
689
|
+
const proofMessage = `CoinPay Wallet Create: ${Date.now()}`;
|
|
690
|
+
const { privateKey: proofKey } = deriveKeyPair(seed, 'ETH', 0);
|
|
691
|
+
const proofSignature = signMessage(proofMessage, proofKey);
|
|
692
|
+
|
|
675
693
|
const result = await client.#request('/web-wallet/create', {
|
|
676
694
|
method: 'POST',
|
|
677
695
|
body: JSON.stringify({
|
|
678
696
|
public_key_secp256k1: publicKeySecp256k1,
|
|
679
697
|
initial_addresses: initialAddresses,
|
|
698
|
+
proof_of_ownership: {
|
|
699
|
+
message: proofMessage,
|
|
700
|
+
signature: proofSignature,
|
|
701
|
+
},
|
|
680
702
|
}),
|
|
681
703
|
});
|
|
682
704
|
|
|
@@ -997,28 +1019,36 @@ export class WalletClient {
|
|
|
997
1019
|
}),
|
|
998
1020
|
});
|
|
999
1021
|
|
|
1000
|
-
// Step 2: Sign the transaction locally
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
//
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
+
// Step 2: Sign the transaction locally.
|
|
1023
|
+
//
|
|
1024
|
+
// NOT IMPLEMENTED, and it now says so instead of pretending.
|
|
1025
|
+
//
|
|
1026
|
+
// This used to call `signMessage(unsignedTx, privateKey)` — a generic
|
|
1027
|
+
// message signature over the JSON of the unsigned transaction — and post
|
|
1028
|
+
// the result as `signed_tx`. That is not a signed transaction on any chain:
|
|
1029
|
+
// a real one is the RLP-encoded (EVM), PSBT-finalised (BTC) or
|
|
1030
|
+
// wire-serialised (Solana) transaction carrying the signature in the right
|
|
1031
|
+
// field. What was produced could never be broadcast, so this method failed
|
|
1032
|
+
// for every integrator who called it — silently, by producing plausible
|
|
1033
|
+
// output that only failed later at the node.
|
|
1034
|
+
//
|
|
1035
|
+
// Failing here rather than guessing is deliberate. Serialising each chain
|
|
1036
|
+
// correctly needs the exact shape of `prepareResult.unsigned_tx` (defined by
|
|
1037
|
+
// the server, not this package) and verification against a live node.
|
|
1038
|
+
// Shipping an unverified serialiser would reintroduce the same class of bug
|
|
1039
|
+
// with more code behind it.
|
|
1040
|
+
//
|
|
1041
|
+
// The primitives to do this yourself are exported and do work:
|
|
1042
|
+
// `signDigest()` for EVM digests and `signMessage()` for EIP-191. Build the
|
|
1043
|
+
// transaction with viem / ethers / @solana/web3.js, sign the digest here,
|
|
1044
|
+
// and post the serialised result to the broadcast endpoint.
|
|
1045
|
+
void prepareResult;
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
'WalletClient.send() is not implemented: chain-specific transaction ' +
|
|
1048
|
+
'serialisation is not available in this package. Build and serialise the ' +
|
|
1049
|
+
'transaction with a chain library, sign the digest with signDigest(), and ' +
|
|
1050
|
+
'POST it to /web-wallet/:id/broadcast. See docs/sdk for an example.'
|
|
1051
|
+
);
|
|
1022
1052
|
}
|
|
1023
1053
|
|
|
1024
1054
|
// ==========================================================
|