@kairoguard/sdk 0.0.1
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 +92 -0
- package/dist/auditBundle.d.ts +28 -0
- package/dist/auditBundle.js +20 -0
- package/dist/backend.d.ts +288 -0
- package/dist/backend.js +107 -0
- package/dist/bitcoinIntent.d.ts +104 -0
- package/dist/bitcoinIntent.js +126 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +237 -0
- package/dist/client.d.ts +186 -0
- package/dist/client.js +767 -0
- package/dist/evm.d.ts +19 -0
- package/dist/evm.js +53 -0
- package/dist/evmIntent.d.ts +11 -0
- package/dist/evmIntent.js +12 -0
- package/dist/ika-protocol.d.ts +85 -0
- package/dist/ika-protocol.js +156 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/keystore.d.ts +29 -0
- package/dist/keystore.js +53 -0
- package/dist/skill-templates.d.ts +9 -0
- package/dist/skill-templates.js +252 -0
- package/dist/solanaIntent.d.ts +128 -0
- package/dist/solanaIntent.js +214 -0
- package/dist/suiCustody.d.ts +14 -0
- package/dist/suiCustody.js +183 -0
- package/dist/suiReceipts.d.ts +27 -0
- package/dist/suiReceipts.js +203 -0
- package/dist/suiResult.d.ts +8 -0
- package/dist/suiResult.js +12 -0
- package/dist/suiTxBuilders.d.ts +15 -0
- package/dist/suiTxBuilders.js +38 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +1 -0
- package/package.json +29 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KairoClient -- the main entry point for agents.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const kairo = new KairoClient({
|
|
6
|
+
* apiKey: "ka_abc123...",
|
|
7
|
+
* });
|
|
8
|
+
* const wallet = await kairo.createWallet();
|
|
9
|
+
* // { walletId: "0xabc...", address: "0x742d...", curve: "secp256k1" }
|
|
10
|
+
*/
|
|
11
|
+
import { BackendClient, } from "./backend.js";
|
|
12
|
+
import { KeyStore } from "./keystore.js";
|
|
13
|
+
import { Curve, fetchProtocolParams, deriveEncryptionKeys, generateSeed, generateSessionIdentifier, runDKG, computeUserOutputSignature, fetchDWallet, } from "./ika-protocol.js";
|
|
14
|
+
import { Hash, SignatureAlgorithm, createUserSignMessageWithPublicOutput } from "@ika.xyz/sdk";
|
|
15
|
+
import { computeEvmIntentFromUnsignedTxBytes } from "./evmIntent.js";
|
|
16
|
+
import { keccak256, recoverTransactionAddress, serializeTransaction, } from "viem";
|
|
17
|
+
const DEFAULT_SUI_RPC = "https://fullnode.testnet.sui.io:443";
|
|
18
|
+
const DEFAULT_EVM_RPC_URLS = {
|
|
19
|
+
1: "https://rpc.ankr.com/eth",
|
|
20
|
+
11155111: "https://rpc.ankr.com/eth_sepolia",
|
|
21
|
+
};
|
|
22
|
+
const DKG_POLL_INTERVAL_MS = 2_000;
|
|
23
|
+
const DKG_POLL_TIMEOUT_MS = 120_000;
|
|
24
|
+
const PRESIGN_POLL_INTERVAL_MS = 2_000;
|
|
25
|
+
const PRESIGN_POLL_TIMEOUT_MS = 120_000;
|
|
26
|
+
const SIGN_POLL_INTERVAL_MS = 2_000;
|
|
27
|
+
const SIGN_POLL_TIMEOUT_MS = 180_000;
|
|
28
|
+
function curveToNumber(curve) {
|
|
29
|
+
return curve === "ed25519" ? 2 : 0;
|
|
30
|
+
}
|
|
31
|
+
export class KairoClient {
|
|
32
|
+
backend;
|
|
33
|
+
store;
|
|
34
|
+
suiRpcUrl;
|
|
35
|
+
network;
|
|
36
|
+
evmRpcUrls;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.backend = new BackendClient({ backendUrl: opts.backendUrl, apiKey: opts.apiKey });
|
|
39
|
+
this.store = new KeyStore(opts.storePath);
|
|
40
|
+
this.suiRpcUrl = opts.suiRpcUrl ?? DEFAULT_SUI_RPC;
|
|
41
|
+
this.network = opts.network ?? "testnet";
|
|
42
|
+
this.evmRpcUrls = { ...DEFAULT_EVM_RPC_URLS, ...(opts.evmRpcUrls ?? {}) };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a new dWallet. Runs DKG on the agent's machine, submits to backend,
|
|
46
|
+
* and optionally provisions the wallet in the vault.
|
|
47
|
+
*
|
|
48
|
+
* The agent's secret share is stored locally and never sent to the server.
|
|
49
|
+
*/
|
|
50
|
+
async createWallet(opts) {
|
|
51
|
+
const curve = opts?.curve ?? "secp256k1";
|
|
52
|
+
// 1. Generate encryption seed and derive keys
|
|
53
|
+
const seed = generateSeed();
|
|
54
|
+
const encryptionKeys = await deriveEncryptionKeys(seed, curve);
|
|
55
|
+
// 2. Fetch protocol params from Ika network (runs locally, avoids backend memory limit)
|
|
56
|
+
const protocolParams = await fetchProtocolParams(curve, this.suiRpcUrl, this.network);
|
|
57
|
+
// 3. Generate session identifier
|
|
58
|
+
const sessionIdentifier = generateSessionIdentifier();
|
|
59
|
+
// 4. Get backend admin address (DKG must target the admin signer)
|
|
60
|
+
const health = await this.backend.getHealth();
|
|
61
|
+
const adminAddress = health.adminAddress;
|
|
62
|
+
// 5. Run client-side DKG
|
|
63
|
+
const dkgOutputs = await runDKG({
|
|
64
|
+
protocolPublicParameters: protocolParams,
|
|
65
|
+
curve,
|
|
66
|
+
encryptionKey: encryptionKeys.encryptionKey,
|
|
67
|
+
sessionIdentifier,
|
|
68
|
+
adminAddress,
|
|
69
|
+
});
|
|
70
|
+
// 6. Submit DKG to backend
|
|
71
|
+
const encKeySignature = await encryptionKeys.getEncryptionKeySignature();
|
|
72
|
+
const submitResult = await this.backend.submitDKG({
|
|
73
|
+
userPublicOutput: dkgOutputs.userPublicOutput,
|
|
74
|
+
userDkgMessage: dkgOutputs.userDKGMessage,
|
|
75
|
+
encryptedUserShareAndProof: dkgOutputs.encryptedUserShareAndProof,
|
|
76
|
+
sessionIdentifier: Array.from(sessionIdentifier),
|
|
77
|
+
signerPublicKey: Array.from(encryptionKeys.getPublicKey().toRawBytes()),
|
|
78
|
+
encryptionKeyAddress: encryptionKeys.getSuiAddress(),
|
|
79
|
+
encryptionKey: Array.from(encryptionKeys.encryptionKey),
|
|
80
|
+
encryptionKeySignature: Array.from(encKeySignature),
|
|
81
|
+
curve: curveToNumber(curve),
|
|
82
|
+
});
|
|
83
|
+
if (!submitResult.success) {
|
|
84
|
+
throw new Error(`DKG submit failed: ${submitResult.requestId}`);
|
|
85
|
+
}
|
|
86
|
+
// 7. Poll for DKG completion
|
|
87
|
+
const dkgResult = await this.pollDKGStatus(submitResult.requestId);
|
|
88
|
+
const walletId = dkgResult.dWalletObjectId;
|
|
89
|
+
const address = (curve === "ed25519" ? dkgResult.solanaAddress : dkgResult.ethereumAddress) ?? "";
|
|
90
|
+
const encryptedShareId = dkgResult.encryptedUserSecretKeyShareId ?? "";
|
|
91
|
+
// 8. Activate the dWallet (sign to accept encrypted key share)
|
|
92
|
+
if (encryptedShareId) {
|
|
93
|
+
await this.activateWallet(walletId, encryptedShareId, encryptionKeys, dkgOutputs.userPublicOutput);
|
|
94
|
+
}
|
|
95
|
+
// 9. Provision into vault (binding + registration) if policy is provided
|
|
96
|
+
let bindingObjectId;
|
|
97
|
+
if (opts?.policyObjectId) {
|
|
98
|
+
const provisionResult = await this.backend.provision({
|
|
99
|
+
dwalletObjectId: walletId,
|
|
100
|
+
policyObjectId: opts.policyObjectId,
|
|
101
|
+
stableId: opts.stableId ?? `agent-wallet-${walletId.slice(0, 8)}`,
|
|
102
|
+
});
|
|
103
|
+
bindingObjectId = provisionResult.bindingObjectId ?? undefined;
|
|
104
|
+
}
|
|
105
|
+
// 10. Save secret share locally
|
|
106
|
+
const record = {
|
|
107
|
+
walletId,
|
|
108
|
+
dWalletCapId: dkgResult.dWalletCapObjectId,
|
|
109
|
+
address,
|
|
110
|
+
curve,
|
|
111
|
+
seed: Array.from(seed),
|
|
112
|
+
userSecretKeyShare: dkgOutputs.userSecretKeyShare,
|
|
113
|
+
userPublicOutput: dkgOutputs.userPublicOutput,
|
|
114
|
+
encryptedUserSecretKeyShareId: encryptedShareId,
|
|
115
|
+
bindingObjectId,
|
|
116
|
+
policyObjectId: opts?.policyObjectId,
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
};
|
|
119
|
+
this.store.save(record);
|
|
120
|
+
return {
|
|
121
|
+
walletId,
|
|
122
|
+
address,
|
|
123
|
+
curve,
|
|
124
|
+
bindingObjectId,
|
|
125
|
+
createdAt: record.createdAt,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** List all wallets in local key store. */
|
|
129
|
+
listWallets() {
|
|
130
|
+
return this.store.list().map((r) => ({
|
|
131
|
+
walletId: r.walletId,
|
|
132
|
+
address: r.address,
|
|
133
|
+
curve: r.curve,
|
|
134
|
+
bindingObjectId: r.bindingObjectId,
|
|
135
|
+
createdAt: r.createdAt,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
/** Get a wallet from local key store by ID. */
|
|
139
|
+
getWallet(walletId) {
|
|
140
|
+
const r = this.store.load(walletId);
|
|
141
|
+
if (!r)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
walletId: r.walletId,
|
|
145
|
+
address: r.address,
|
|
146
|
+
curve: r.curve,
|
|
147
|
+
bindingObjectId: r.bindingObjectId,
|
|
148
|
+
createdAt: r.createdAt,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Governance-first policy update: creates a new policy + version, then proposes
|
|
153
|
+
* a change for approvers. This method does NOT execute/reaffirm directly.
|
|
154
|
+
*/
|
|
155
|
+
async updatePolicy(params) {
|
|
156
|
+
return this.proposePolicyUpdate(params);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create and register a new policy version, then create governance proposal.
|
|
160
|
+
* This is the safe default for agents so policy changes require approvers.
|
|
161
|
+
*/
|
|
162
|
+
async proposePolicyUpdate(params) {
|
|
163
|
+
const wallet = this.requireWalletRecord(params.walletId);
|
|
164
|
+
if (!wallet.bindingObjectId) {
|
|
165
|
+
throw new Error("Wallet is missing bindingObjectId. Provision the wallet before proposing policy updates.");
|
|
166
|
+
}
|
|
167
|
+
const createBody = {
|
|
168
|
+
stableId: params.stableId,
|
|
169
|
+
version: params.version,
|
|
170
|
+
expiresAtMs: params.expiresAtMs,
|
|
171
|
+
allowNamespaces: params.allowNamespaces,
|
|
172
|
+
allowChainIds: params.allowChainIds,
|
|
173
|
+
allowDestinations: params.allowDestinations,
|
|
174
|
+
denyDestinations: params.denyDestinations,
|
|
175
|
+
rules: params.rules,
|
|
176
|
+
};
|
|
177
|
+
const created = await this.backend.createPolicyV4(createBody);
|
|
178
|
+
if (!created.success || !created.policyObjectId?.startsWith("0x")) {
|
|
179
|
+
throw new Error(created.error ?? "Failed to create policy");
|
|
180
|
+
}
|
|
181
|
+
const registered = await this.backend.registerPolicyVersionFromPolicy({
|
|
182
|
+
policyObjectId: created.policyObjectId,
|
|
183
|
+
note: params.note ?? `sdk policy update ${params.version}`,
|
|
184
|
+
registryObjectId: params.registryObjectId,
|
|
185
|
+
});
|
|
186
|
+
if (!registered.success || !registered.policyVersionObjectId?.startsWith("0x")) {
|
|
187
|
+
throw new Error(registered.error ?? "Failed to register policy version");
|
|
188
|
+
}
|
|
189
|
+
const proposed = await this.backend.proposeGovernancePolicyChange({
|
|
190
|
+
governanceId: params.governanceId,
|
|
191
|
+
bindingId: wallet.bindingObjectId,
|
|
192
|
+
targetVersionId: registered.policyVersionObjectId,
|
|
193
|
+
});
|
|
194
|
+
if (!proposed.success || !proposed.proposalId?.startsWith("0x")) {
|
|
195
|
+
throw new Error(proposed.error ?? "Failed to propose governance policy change");
|
|
196
|
+
}
|
|
197
|
+
// Track latest policy target locally; governance execution still required.
|
|
198
|
+
this.store.save({
|
|
199
|
+
...wallet,
|
|
200
|
+
policyObjectId: created.policyObjectId,
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
walletId: params.walletId,
|
|
204
|
+
governanceId: params.governanceId,
|
|
205
|
+
bindingObjectId: wallet.bindingObjectId,
|
|
206
|
+
policyObjectId: created.policyObjectId,
|
|
207
|
+
policyVersionObjectId: registered.policyVersionObjectId,
|
|
208
|
+
proposalId: proposed.proposalId,
|
|
209
|
+
policyDigest: created.digest,
|
|
210
|
+
registerDigest: registered.digest,
|
|
211
|
+
proposalDigest: proposed.digest,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async approvePolicyUpdate(params) {
|
|
215
|
+
const r = await this.backend.approveGovernancePolicyChange(params);
|
|
216
|
+
if (!r.success) {
|
|
217
|
+
throw new Error(r.error ?? "Failed to approve policy update proposal");
|
|
218
|
+
}
|
|
219
|
+
return { digest: r.digest };
|
|
220
|
+
}
|
|
221
|
+
async executePolicyUpdate(params) {
|
|
222
|
+
const wallet = this.requireWalletRecord(params.walletId);
|
|
223
|
+
if (!wallet.bindingObjectId) {
|
|
224
|
+
throw new Error("Wallet is missing bindingObjectId. Provision the wallet before executing policy updates.");
|
|
225
|
+
}
|
|
226
|
+
const r = await this.backend.executeAndReaffirmGovernancePolicyChange({
|
|
227
|
+
governanceId: params.governanceId,
|
|
228
|
+
proposalId: params.proposalId,
|
|
229
|
+
bindingObjectId: wallet.bindingObjectId,
|
|
230
|
+
});
|
|
231
|
+
if (!r.success) {
|
|
232
|
+
throw new Error(r.error ?? "Failed to execute and reaffirm policy update");
|
|
233
|
+
}
|
|
234
|
+
return { digest: r.digest };
|
|
235
|
+
}
|
|
236
|
+
async getPolicyUpdateStatus(proposalId) {
|
|
237
|
+
const proposalResp = await this.backend.getGovernanceProposal(proposalId);
|
|
238
|
+
if (!proposalResp.success || !proposalResp.proposal) {
|
|
239
|
+
throw new Error(proposalResp.error ?? "Failed to fetch governance proposal");
|
|
240
|
+
}
|
|
241
|
+
const proposal = proposalResp.proposal;
|
|
242
|
+
let threshold;
|
|
243
|
+
let timelockDurationMs;
|
|
244
|
+
if (proposal.governanceId?.startsWith("0x")) {
|
|
245
|
+
const govResp = await this.backend.getGovernance(proposal.governanceId);
|
|
246
|
+
if (govResp.success && govResp.governance) {
|
|
247
|
+
threshold = Number(govResp.governance.threshold);
|
|
248
|
+
timelockDurationMs = Number(govResp.governance.timelockDurationMs);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const approvalsCollected = proposal.approvals?.length ?? 0;
|
|
252
|
+
const approvalsNeeded = threshold !== undefined ? Math.max(0, threshold - approvalsCollected) : undefined;
|
|
253
|
+
let state = "awaiting_approvals";
|
|
254
|
+
if (proposal.cancelled) {
|
|
255
|
+
state = "cancelled";
|
|
256
|
+
}
|
|
257
|
+
else if (proposal.executed) {
|
|
258
|
+
state = "executed";
|
|
259
|
+
}
|
|
260
|
+
else if (threshold !== undefined && approvalsCollected < threshold) {
|
|
261
|
+
state = "awaiting_approvals";
|
|
262
|
+
}
|
|
263
|
+
else if (proposal.thresholdMetAtMs > 0 &&
|
|
264
|
+
timelockDurationMs !== undefined &&
|
|
265
|
+
timelockDurationMs > 0 &&
|
|
266
|
+
Date.now() < proposal.thresholdMetAtMs + timelockDurationMs) {
|
|
267
|
+
state = "awaiting_timelock";
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
state = "ready_to_execute";
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
proposal,
|
|
274
|
+
threshold,
|
|
275
|
+
timelockDurationMs,
|
|
276
|
+
approvalsCollected,
|
|
277
|
+
approvalsNeeded,
|
|
278
|
+
state,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async getPolicy(walletId) {
|
|
282
|
+
const wallet = this.requireWalletRecord(walletId);
|
|
283
|
+
if (!wallet.policyObjectId) {
|
|
284
|
+
throw new Error("Wallet has no policyObjectId recorded");
|
|
285
|
+
}
|
|
286
|
+
const response = await this.backend.getPolicy(wallet.policyObjectId);
|
|
287
|
+
if (!response.success || !response.policy) {
|
|
288
|
+
throw new Error(response.error ?? "Failed to fetch policy");
|
|
289
|
+
}
|
|
290
|
+
return response.policy;
|
|
291
|
+
}
|
|
292
|
+
async createPresign(walletId) {
|
|
293
|
+
const wallet = this.requireWalletRecord(walletId);
|
|
294
|
+
const req = await this.backend.requestPresign({
|
|
295
|
+
dWalletId: wallet.walletId,
|
|
296
|
+
});
|
|
297
|
+
if (!req.success) {
|
|
298
|
+
throw new Error(`Failed to request presign for wallet ${walletId}`);
|
|
299
|
+
}
|
|
300
|
+
const status = await this.pollPresignStatus(req.requestId);
|
|
301
|
+
return {
|
|
302
|
+
requestId: req.requestId,
|
|
303
|
+
presignId: status.presignId,
|
|
304
|
+
presignBytes: new Uint8Array(status.presignBytes),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async sign(walletId, messageHex, opts) {
|
|
308
|
+
const wallet = this.requireWalletRecord(walletId);
|
|
309
|
+
if (wallet.curve !== "secp256k1") {
|
|
310
|
+
throw new Error(`sign() currently supports secp256k1 wallets only (wallet curve: ${wallet.curve})`);
|
|
311
|
+
}
|
|
312
|
+
const messageHexNoPrefix = stripHexPrefix(messageHex);
|
|
313
|
+
if (!messageHexNoPrefix || messageHexNoPrefix.length % 2 !== 0) {
|
|
314
|
+
throw new Error("messageHex must be a non-empty even-length hex string");
|
|
315
|
+
}
|
|
316
|
+
let presignId = opts?.presignId;
|
|
317
|
+
let presignBytes = opts?.presignBytes;
|
|
318
|
+
if (!presignId || !presignBytes) {
|
|
319
|
+
if (presignId || presignBytes) {
|
|
320
|
+
throw new Error("When overriding presign, both presignId and presignBytes are required");
|
|
321
|
+
}
|
|
322
|
+
const presign = await this.createPresign(walletId);
|
|
323
|
+
presignId = presign.presignId;
|
|
324
|
+
presignBytes = presign.presignBytes;
|
|
325
|
+
}
|
|
326
|
+
const protocolParams = await fetchProtocolParams("secp256k1", this.suiRpcUrl, this.network);
|
|
327
|
+
const messageBytes = new Uint8Array(Buffer.from(messageHexNoPrefix, "hex"));
|
|
328
|
+
const userSignMessage = await this.computeUserSignMessageWithExtensionFallback(wallet, protocolParams, presignBytes, messageBytes);
|
|
329
|
+
const policyContext = opts?.policyContext ?? {
|
|
330
|
+
namespace: 1,
|
|
331
|
+
chainId: 1,
|
|
332
|
+
intentHashHex: keccak256(ensureHexPrefix(messageHexNoPrefix)),
|
|
333
|
+
destinationHex: "0x0000000000000000000000000000000000000000",
|
|
334
|
+
nativeValue: 0n,
|
|
335
|
+
};
|
|
336
|
+
const policyReceiptId = await this.mintPolicyReceipt(wallet, policyContext);
|
|
337
|
+
const dWalletCapId = wallet.dWalletCapId;
|
|
338
|
+
if (!dWalletCapId) {
|
|
339
|
+
throw new Error("Wallet record is missing dWalletCapId. Recreate/provision this wallet before signing.");
|
|
340
|
+
}
|
|
341
|
+
const req = await this.backend.requestSign({
|
|
342
|
+
dWalletId: wallet.walletId,
|
|
343
|
+
dWalletCapId,
|
|
344
|
+
encryptedUserSecretKeyShareId: wallet.encryptedUserSecretKeyShareId ?? "",
|
|
345
|
+
userOutputSignature: [],
|
|
346
|
+
presignId,
|
|
347
|
+
messageHex: messageHexNoPrefix,
|
|
348
|
+
userSignMessage: Array.from(userSignMessage),
|
|
349
|
+
policyReceiptId,
|
|
350
|
+
policyBindingObjectId: wallet.bindingObjectId,
|
|
351
|
+
policyObjectId: wallet.policyObjectId,
|
|
352
|
+
policyVersion: opts?.policyVersion ?? "1.0.0",
|
|
353
|
+
ethTx: opts?.ethTx,
|
|
354
|
+
});
|
|
355
|
+
if (!req.success) {
|
|
356
|
+
throw new Error(`Failed to request sign for wallet ${walletId}`);
|
|
357
|
+
}
|
|
358
|
+
const signStatus = await this.pollSignStatus(req.requestId);
|
|
359
|
+
return {
|
|
360
|
+
requestId: req.requestId,
|
|
361
|
+
signId: signStatus.signId,
|
|
362
|
+
presignId,
|
|
363
|
+
signatureHex: ensureHexPrefix(signStatus.signatureHex),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async signEvm(params) {
|
|
367
|
+
const wallet = this.requireWalletRecord(params.walletId);
|
|
368
|
+
const rpcUrl = this.resolveEvmRpcUrl(params.chainId, params.rpcUrl);
|
|
369
|
+
const value = toBigInt(params.value);
|
|
370
|
+
const nonceHex = await this.evmRpcCall(rpcUrl, "eth_getTransactionCount", [
|
|
371
|
+
wallet.address,
|
|
372
|
+
"pending",
|
|
373
|
+
]);
|
|
374
|
+
const nonce = Number(BigInt(nonceHex));
|
|
375
|
+
const data = params.data ?? "0x";
|
|
376
|
+
const gasHex = await this.evmRpcCall(rpcUrl, "eth_estimateGas", [
|
|
377
|
+
{
|
|
378
|
+
from: wallet.address,
|
|
379
|
+
to: params.to,
|
|
380
|
+
value: ensureHexPrefix(value.toString(16)),
|
|
381
|
+
data,
|
|
382
|
+
},
|
|
383
|
+
]);
|
|
384
|
+
const gas = BigInt(gasHex);
|
|
385
|
+
const gasPriceHex = await this.evmRpcCall(rpcUrl, "eth_gasPrice", []);
|
|
386
|
+
const gasPrice = BigInt(gasPriceHex);
|
|
387
|
+
const maxPriorityFeePerGas = gasPrice;
|
|
388
|
+
const maxFeePerGas = gasPrice * 2n;
|
|
389
|
+
const unsignedTx = {
|
|
390
|
+
type: "eip1559",
|
|
391
|
+
chainId: params.chainId,
|
|
392
|
+
nonce,
|
|
393
|
+
to: params.to,
|
|
394
|
+
value,
|
|
395
|
+
data,
|
|
396
|
+
gas,
|
|
397
|
+
maxFeePerGas,
|
|
398
|
+
maxPriorityFeePerGas,
|
|
399
|
+
};
|
|
400
|
+
const serializedUnsigned = serializeTransaction(unsignedTx);
|
|
401
|
+
const evmIntent = computeEvmIntentFromUnsignedTxBytes({
|
|
402
|
+
chainId: params.chainId,
|
|
403
|
+
unsignedTxBytesHex: serializedUnsigned,
|
|
404
|
+
});
|
|
405
|
+
const signResult = await this.sign(params.walletId, serializedUnsigned, {
|
|
406
|
+
policyContext: {
|
|
407
|
+
namespace: 1,
|
|
408
|
+
chainId: params.chainId,
|
|
409
|
+
intentHashHex: evmIntent.intentHash,
|
|
410
|
+
destinationHex: params.to,
|
|
411
|
+
nativeValue: value,
|
|
412
|
+
},
|
|
413
|
+
ethTx: {
|
|
414
|
+
to: params.to,
|
|
415
|
+
value: value.toString(),
|
|
416
|
+
nonce,
|
|
417
|
+
gasLimit: gas.toString(),
|
|
418
|
+
maxFeePerGas: maxFeePerGas.toString(),
|
|
419
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas.toString(),
|
|
420
|
+
chainId: params.chainId,
|
|
421
|
+
from: wallet.address,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
const sigNoPrefix = stripHexPrefix(signResult.signatureHex);
|
|
425
|
+
let signedTx = null;
|
|
426
|
+
// Ika/backend may return either:
|
|
427
|
+
// - 64-byte compact ECDSA (r||s) without recovery id
|
|
428
|
+
// - 65-byte recoverable (r||s||v/yParity)
|
|
429
|
+
if (sigNoPrefix.length === 128) {
|
|
430
|
+
const r = ensureHexPrefix(sigNoPrefix.slice(0, 64));
|
|
431
|
+
const s = ensureHexPrefix(sigNoPrefix.slice(64, 128));
|
|
432
|
+
for (const yParity of [0, 1]) {
|
|
433
|
+
const candidate = serializeTransaction(unsignedTx, { r, s, yParity });
|
|
434
|
+
try {
|
|
435
|
+
const recovered = await recoverTransactionAddress({
|
|
436
|
+
serializedTransaction: candidate,
|
|
437
|
+
});
|
|
438
|
+
if (recovered.toLowerCase() === wallet.address.toLowerCase()) {
|
|
439
|
+
signedTx = candidate;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Try the other parity.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!signedTx) {
|
|
448
|
+
throw new Error("Failed to recover correct signer address from 64-byte signature");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else if (sigNoPrefix.length === 130) {
|
|
452
|
+
const r = ensureHexPrefix(sigNoPrefix.slice(0, 64));
|
|
453
|
+
const s = ensureHexPrefix(sigNoPrefix.slice(64, 128));
|
|
454
|
+
const vByte = Number.parseInt(sigNoPrefix.slice(128, 130), 16);
|
|
455
|
+
const yParity = (vByte === 27 || vByte === 28 ? vByte - 27 : vByte);
|
|
456
|
+
if (yParity !== 0 && yParity !== 1) {
|
|
457
|
+
throw new Error(`Unsupported signature v/yParity byte: ${vByte}`);
|
|
458
|
+
}
|
|
459
|
+
signedTx = serializeTransaction(unsignedTx, { r, s, yParity });
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
throw new Error(`Unexpected signature length from sign endpoint: ${sigNoPrefix.length / 2} bytes`);
|
|
463
|
+
}
|
|
464
|
+
return signedTx;
|
|
465
|
+
}
|
|
466
|
+
async broadcastEvm(signedTx, rpcUrl) {
|
|
467
|
+
const txHash = await this.evmRpcCall(rpcUrl, "eth_sendRawTransaction", [signedTx]);
|
|
468
|
+
return ensureHexPrefix(txHash);
|
|
469
|
+
}
|
|
470
|
+
async getBalance(address, rpcUrl) {
|
|
471
|
+
const balanceHex = await this.evmRpcCall(rpcUrl, "eth_getBalance", [address, "latest"]);
|
|
472
|
+
return BigInt(balanceHex);
|
|
473
|
+
}
|
|
474
|
+
async activateWallet(walletId, encryptedShareId, encryptionKeys, userPublicOutput) {
|
|
475
|
+
// Wait a moment for the dWallet state to propagate
|
|
476
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
477
|
+
const dWallet = await fetchDWallet(this.suiRpcUrl, this.network, walletId);
|
|
478
|
+
const signature = await computeUserOutputSignature({
|
|
479
|
+
encryptionKeys,
|
|
480
|
+
dWallet,
|
|
481
|
+
userPublicOutput: new Uint8Array(userPublicOutput),
|
|
482
|
+
});
|
|
483
|
+
await this.backend.activateDWallet({
|
|
484
|
+
dWalletId: walletId,
|
|
485
|
+
encryptedUserSecretKeyShareId: encryptedShareId,
|
|
486
|
+
userOutputSignature: Array.from(signature),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async pollDKGStatus(requestId) {
|
|
490
|
+
const deadline = Date.now() + DKG_POLL_TIMEOUT_MS;
|
|
491
|
+
while (Date.now() < deadline) {
|
|
492
|
+
const status = await this.backend.getDKGStatus(requestId);
|
|
493
|
+
if (status.status === "completed") {
|
|
494
|
+
if (!status.dWalletObjectId) {
|
|
495
|
+
throw new Error("DKG completed but no dWalletObjectId returned");
|
|
496
|
+
}
|
|
497
|
+
return status;
|
|
498
|
+
}
|
|
499
|
+
if (status.status === "failed") {
|
|
500
|
+
throw new Error(`DKG failed: ${status.error ?? "unknown error"}`);
|
|
501
|
+
}
|
|
502
|
+
await new Promise((r) => setTimeout(r, DKG_POLL_INTERVAL_MS));
|
|
503
|
+
}
|
|
504
|
+
throw new Error(`DKG timed out after ${DKG_POLL_TIMEOUT_MS / 1000}s (requestId: ${requestId})`);
|
|
505
|
+
}
|
|
506
|
+
requireWalletRecord(walletId) {
|
|
507
|
+
const wallet = this.store.load(walletId);
|
|
508
|
+
if (!wallet) {
|
|
509
|
+
throw new Error(`Wallet not found in local key store: ${walletId}`);
|
|
510
|
+
}
|
|
511
|
+
return wallet;
|
|
512
|
+
}
|
|
513
|
+
async pollPresignStatus(requestId) {
|
|
514
|
+
const deadline = Date.now() + PRESIGN_POLL_TIMEOUT_MS;
|
|
515
|
+
while (Date.now() < deadline) {
|
|
516
|
+
const status = await this.backend.getPresignStatus(requestId);
|
|
517
|
+
if (status.status === "completed") {
|
|
518
|
+
if (!status.presignId || !status.presignBytes) {
|
|
519
|
+
throw new Error("Presign completed but missing presignId/presignBytes");
|
|
520
|
+
}
|
|
521
|
+
return status;
|
|
522
|
+
}
|
|
523
|
+
if (status.status === "failed") {
|
|
524
|
+
throw new Error(`Presign failed: ${status.error ?? "unknown error"}`);
|
|
525
|
+
}
|
|
526
|
+
await sleep(PRESIGN_POLL_INTERVAL_MS);
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`Presign timed out after ${PRESIGN_POLL_TIMEOUT_MS / 1000}s (requestId: ${requestId})`);
|
|
529
|
+
}
|
|
530
|
+
async pollSignStatus(requestId) {
|
|
531
|
+
const deadline = Date.now() + SIGN_POLL_TIMEOUT_MS;
|
|
532
|
+
while (Date.now() < deadline) {
|
|
533
|
+
const status = await this.backend.getSignStatus(requestId);
|
|
534
|
+
if (status.status === "completed") {
|
|
535
|
+
if (!status.signatureHex) {
|
|
536
|
+
throw new Error("Sign completed but no signatureHex returned");
|
|
537
|
+
}
|
|
538
|
+
return status;
|
|
539
|
+
}
|
|
540
|
+
if (status.status === "failed") {
|
|
541
|
+
throw new Error(`Sign failed: ${status.error ?? "unknown error"}`);
|
|
542
|
+
}
|
|
543
|
+
await sleep(SIGN_POLL_INTERVAL_MS);
|
|
544
|
+
}
|
|
545
|
+
throw new Error(`Sign timed out after ${SIGN_POLL_TIMEOUT_MS / 1000}s (requestId: ${requestId})`);
|
|
546
|
+
}
|
|
547
|
+
async mintPolicyReceipt(wallet, ctx) {
|
|
548
|
+
if (!wallet.policyObjectId || !wallet.bindingObjectId) {
|
|
549
|
+
throw new Error("Wallet is missing policy binding metadata. Ensure it is provisioned with policyObjectId and bindingObjectId.");
|
|
550
|
+
}
|
|
551
|
+
const response = await this.backend.mintReceipt({
|
|
552
|
+
policyObjectId: wallet.policyObjectId,
|
|
553
|
+
bindingObjectId: wallet.bindingObjectId,
|
|
554
|
+
namespace: ctx.namespace,
|
|
555
|
+
// chainId is encoded as u64 bytes (16 hex chars)
|
|
556
|
+
chainId: ctx.chainId.toString(16).padStart(16, "0"),
|
|
557
|
+
intentHashHex: stripHexPrefix(ctx.intentHashHex),
|
|
558
|
+
destinationHex: stripHexPrefix(ctx.destinationHex),
|
|
559
|
+
nativeValueHex: ctx.nativeValue.toString(16).padStart(64, "0"),
|
|
560
|
+
contextDataHex: ctx.contextDataHex ? stripHexPrefix(ctx.contextDataHex) : undefined,
|
|
561
|
+
});
|
|
562
|
+
if (response.success === false) {
|
|
563
|
+
throw new Error(String(response.error ?? "Failed to mint policy receipt"));
|
|
564
|
+
}
|
|
565
|
+
if (response.allowed === false) {
|
|
566
|
+
throw new Error("Policy denied this signing intent");
|
|
567
|
+
}
|
|
568
|
+
const receiptId = String(response.receiptId ?? response.receiptObjectId ?? "");
|
|
569
|
+
if (!receiptId.startsWith("0x")) {
|
|
570
|
+
throw new Error("Policy receipt mint succeeded but no receipt id was returned");
|
|
571
|
+
}
|
|
572
|
+
return receiptId;
|
|
573
|
+
}
|
|
574
|
+
async computeUserSignMessageWithExtensionFallback(wallet, protocolParams, presignBytes, messageBytes) {
|
|
575
|
+
try {
|
|
576
|
+
return await createUserSignMessageWithPublicOutput(protocolParams, new Uint8Array(wallet.userPublicOutput), new Uint8Array(wallet.userSecretKeyShare), presignBytes, messageBytes, Hash.KECCAK256, SignatureAlgorithm.ECDSASecp256k1, Curve.SECP256K1);
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
580
|
+
const likelyPresignDecodeIssue = msg.includes("unexpected end of input") ||
|
|
581
|
+
msg.includes("create_sign_centralized_party_message");
|
|
582
|
+
if (!likelyPresignDecodeIssue) {
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
const fallback = await this.rebuildSigningMaterialFromChain(wallet, protocolParams);
|
|
586
|
+
return createUserSignMessageWithPublicOutput(protocolParams, fallback.verifiedPublicOutput, fallback.secretShare, presignBytes, messageBytes, Hash.KECCAK256, SignatureAlgorithm.ECDSASecp256k1, Curve.SECP256K1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async rebuildSigningMaterialFromChain(wallet, protocolParams) {
|
|
590
|
+
if (!wallet.seed?.length) {
|
|
591
|
+
throw new Error("Wallet is missing seed; cannot rebuild signing material from chain");
|
|
592
|
+
}
|
|
593
|
+
if (!wallet.encryptedUserSecretKeyShareId?.startsWith("0x")) {
|
|
594
|
+
throw new Error("Wallet is missing encryptedUserSecretKeyShareId; cannot rebuild signing material from chain");
|
|
595
|
+
}
|
|
596
|
+
const encryptionKeys = await deriveEncryptionKeys(new Uint8Array(wallet.seed), "secp256k1");
|
|
597
|
+
const dWalletResp = await this.backend.getDWalletFull(wallet.walletId);
|
|
598
|
+
if (!dWalletResp.success || !dWalletResp.dWallet) {
|
|
599
|
+
throw new Error(dWalletResp.error ?? "Failed to fetch dWallet for fallback signing");
|
|
600
|
+
}
|
|
601
|
+
const encObj = await this.backend.getSuiObject(wallet.encryptedUserSecretKeyShareId);
|
|
602
|
+
if (!encObj.success) {
|
|
603
|
+
throw new Error(encObj.error ?? "Failed to fetch encrypted share object for fallback signing");
|
|
604
|
+
}
|
|
605
|
+
const fields = (encObj.object?.data?.content?.fields ?? {});
|
|
606
|
+
const normalized = buildNormalizedEncryptedShare(fields);
|
|
607
|
+
const { secretShare, verifiedPublicOutput } = await encryptionKeys.decryptUserShare(dWalletResp.dWallet, normalized, protocolParams);
|
|
608
|
+
return { secretShare, verifiedPublicOutput };
|
|
609
|
+
}
|
|
610
|
+
resolveEvmRpcUrl(chainId, explicitRpcUrl) {
|
|
611
|
+
const url = explicitRpcUrl ?? this.evmRpcUrls[chainId];
|
|
612
|
+
if (!url) {
|
|
613
|
+
throw new Error(`No EVM RPC URL configured for chainId ${chainId}`);
|
|
614
|
+
}
|
|
615
|
+
return url;
|
|
616
|
+
}
|
|
617
|
+
async evmRpcCall(rpcUrl, method, params) {
|
|
618
|
+
const res = await fetch(rpcUrl, {
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: { "Content-Type": "application/json" },
|
|
621
|
+
body: JSON.stringify({
|
|
622
|
+
jsonrpc: "2.0",
|
|
623
|
+
id: Date.now(),
|
|
624
|
+
method,
|
|
625
|
+
params,
|
|
626
|
+
}),
|
|
627
|
+
});
|
|
628
|
+
const json = await res.json();
|
|
629
|
+
if (!res.ok) {
|
|
630
|
+
throw new Error(`EVM RPC request failed (${method}): HTTP ${res.status}`);
|
|
631
|
+
}
|
|
632
|
+
if (json.error) {
|
|
633
|
+
throw new Error(`EVM RPC error (${method}): ${json.error.message ?? "unknown error"}`);
|
|
634
|
+
}
|
|
635
|
+
if (json.result === undefined) {
|
|
636
|
+
throw new Error(`EVM RPC response missing result (${method})`);
|
|
637
|
+
}
|
|
638
|
+
return json.result;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function sleep(ms) {
|
|
642
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
643
|
+
}
|
|
644
|
+
function stripHexPrefix(value) {
|
|
645
|
+
return value.startsWith("0x") ? value.slice(2) : value;
|
|
646
|
+
}
|
|
647
|
+
function ensureHexPrefix(value) {
|
|
648
|
+
return (value.startsWith("0x") ? value : `0x${value}`);
|
|
649
|
+
}
|
|
650
|
+
function toBigInt(value) {
|
|
651
|
+
if (typeof value === "bigint")
|
|
652
|
+
return value;
|
|
653
|
+
if (typeof value === "number")
|
|
654
|
+
return BigInt(value);
|
|
655
|
+
return value.startsWith("0x") ? BigInt(value) : BigInt(value);
|
|
656
|
+
}
|
|
657
|
+
function buildNormalizedEncryptedShare(fields) {
|
|
658
|
+
const { state } = normalizeMoveEnumState(fields.state);
|
|
659
|
+
const candidate = {
|
|
660
|
+
state,
|
|
661
|
+
encryption_key_address: String(fields.encryption_key_address ?? fields.encryptionKeyAddress ?? ""),
|
|
662
|
+
encrypted_centralized_secret_share_and_proof: fields.encrypted_centralized_secret_share_and_proof ??
|
|
663
|
+
fields.encryptedCentralizedSecretShareAndProof ??
|
|
664
|
+
fields.encrypted_user_share_and_proof ??
|
|
665
|
+
[],
|
|
666
|
+
};
|
|
667
|
+
const rawSig = candidate?.state?.KeyHolderSigned?.user_output_signature ??
|
|
668
|
+
candidate?.state?.KeyHolderSigned?.fields?.user_output_signature ??
|
|
669
|
+
candidate?.state?.user_output_signature ??
|
|
670
|
+
findNestedUserOutputSignature(fields.state) ??
|
|
671
|
+
null;
|
|
672
|
+
const sig = normalizeBytesLike(rawSig);
|
|
673
|
+
if (sig && sig.length > 0) {
|
|
674
|
+
candidate.state = {
|
|
675
|
+
KeyHolderSigned: {
|
|
676
|
+
...(candidate.state?.KeyHolderSigned ?? {}),
|
|
677
|
+
user_output_signature: sig,
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return candidate;
|
|
682
|
+
}
|
|
683
|
+
function normalizeBytesLike(v) {
|
|
684
|
+
if (Array.isArray(v) && v.every((x) => Number.isInteger(x) && x >= 0 && x <= 255)) {
|
|
685
|
+
return v;
|
|
686
|
+
}
|
|
687
|
+
if (typeof v === "string" && v.length > 0) {
|
|
688
|
+
try {
|
|
689
|
+
return Array.from(Buffer.from(v, "base64"));
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (v && typeof v === "object") {
|
|
696
|
+
const o = v;
|
|
697
|
+
if (Array.isArray(o.bytes))
|
|
698
|
+
return normalizeBytesLike(o.bytes);
|
|
699
|
+
if (Array.isArray(o.data))
|
|
700
|
+
return normalizeBytesLike(o.data);
|
|
701
|
+
if (Array.isArray(o.value))
|
|
702
|
+
return normalizeBytesLike(o.value);
|
|
703
|
+
if (o.fields)
|
|
704
|
+
return normalizeBytesLike(o.fields);
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
function normalizeMoveEnumState(stateRaw) {
|
|
709
|
+
if (!stateRaw || typeof stateRaw !== "object")
|
|
710
|
+
return { kind: "Unknown", state: {} };
|
|
711
|
+
const stateObj = stateRaw;
|
|
712
|
+
if (stateObj.fields && typeof stateObj.fields === "object") {
|
|
713
|
+
return normalizeMoveEnumState(stateObj.fields);
|
|
714
|
+
}
|
|
715
|
+
if (typeof stateObj.$kind === "string") {
|
|
716
|
+
const kind = String(stateObj.$kind);
|
|
717
|
+
const copy = { ...stateObj };
|
|
718
|
+
delete copy.$kind;
|
|
719
|
+
const flat = copy.fields && typeof copy.fields === "object"
|
|
720
|
+
? { ...copy.fields }
|
|
721
|
+
: copy;
|
|
722
|
+
return { kind, state: { [kind]: flat } };
|
|
723
|
+
}
|
|
724
|
+
const keys = Object.keys(stateObj);
|
|
725
|
+
if (!keys.length)
|
|
726
|
+
return { kind: "Unknown", state: {} };
|
|
727
|
+
const kind = String(keys[0]);
|
|
728
|
+
const inner = stateObj[kind];
|
|
729
|
+
const flat = inner &&
|
|
730
|
+
typeof inner === "object" &&
|
|
731
|
+
inner.fields &&
|
|
732
|
+
typeof inner.fields === "object"
|
|
733
|
+
? { ...inner.fields }
|
|
734
|
+
: (inner ?? {});
|
|
735
|
+
return { kind, state: { [kind]: flat } };
|
|
736
|
+
}
|
|
737
|
+
function findNestedUserOutputSignature(v, depth = 0) {
|
|
738
|
+
if (depth > 6 || v == null || typeof v !== "object")
|
|
739
|
+
return null;
|
|
740
|
+
if (Array.isArray(v)) {
|
|
741
|
+
for (const it of v) {
|
|
742
|
+
const r = findNestedUserOutputSignature(it, depth + 1);
|
|
743
|
+
if (r != null)
|
|
744
|
+
return r;
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
const obj = v;
|
|
749
|
+
if (obj.user_output_signature != null)
|
|
750
|
+
return obj.user_output_signature;
|
|
751
|
+
if (obj.fields != null) {
|
|
752
|
+
const r = findNestedUserOutputSignature(obj.fields, depth + 1);
|
|
753
|
+
if (r != null)
|
|
754
|
+
return r;
|
|
755
|
+
}
|
|
756
|
+
if (obj.KeyHolderSigned != null) {
|
|
757
|
+
const r = findNestedUserOutputSignature(obj.KeyHolderSigned, depth + 1);
|
|
758
|
+
if (r != null)
|
|
759
|
+
return r;
|
|
760
|
+
}
|
|
761
|
+
for (const k of Object.keys(obj)) {
|
|
762
|
+
const r = findNestedUserOutputSignature(obj[k], depth + 1);
|
|
763
|
+
if (r != null)
|
|
764
|
+
return r;
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|