@kaspacom/kcc20-tx-builder 0.2.5 → 0.4.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 +21 -0
- package/README.md +54 -8
- package/RELEASING.md +93 -0
- package/SECURITY.md +16 -0
- package/artifacts/KCC20SnapshotBootstrap.placeholder.json +101677 -0
- package/artifacts/KCC20SnapshotController.placeholder.json +46547 -0
- package/artifacts/kcc20-snapshot-bootstrap/KCC20SnapshotBootstrap.placeholder.json +101677 -0
- package/artifacts/kcc20-snapshot-controller/KCC20SnapshotController.placeholder.json +46547 -0
- package/artifacts/snapshot-standards.lock.json +56 -0
- package/dist/artifacts.d.ts +31 -0
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +31 -0
- package/dist/artifacts.js.map +1 -1
- package/dist/cjs/artifacts.d.ts +31 -0
- package/dist/cjs/artifacts.js +32 -1
- package/dist/cjs/artifacts.js.map +1 -1
- package/dist/cjs/engine.js +58 -44
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/operations.d.ts +1 -1
- package/dist/cjs/operations.js +4 -0
- package/dist/cjs/operations.js.map +1 -1
- package/dist/cjs/sale-common.d.ts +1 -1
- package/dist/cjs/sale-common.js +39 -4
- package/dist/cjs/sale-common.js.map +1 -1
- package/dist/cjs/snapshot-deployment.d.ts +82 -0
- package/dist/cjs/snapshot-deployment.js +82 -0
- package/dist/cjs/snapshot-deployment.js.map +1 -0
- package/dist/cjs/snapshot-operation.d.ts +27 -0
- package/dist/cjs/snapshot-operation.js +2433 -0
- package/dist/cjs/snapshot-operation.js.map +1 -0
- package/dist/cjs/snapshot-signerless-transactions.d.ts +132 -0
- package/dist/cjs/snapshot-signerless-transactions.js +746 -0
- package/dist/cjs/snapshot-signerless-transactions.js.map +1 -0
- package/dist/cjs/snapshot-signerless.d.ts +226 -0
- package/dist/cjs/snapshot-signerless.js +1022 -0
- package/dist/cjs/snapshot-signerless.js.map +1 -0
- package/dist/cjs/snapshot-verification.d.ts +57 -0
- package/dist/cjs/snapshot-verification.js +96 -0
- package/dist/cjs/snapshot-verification.js.map +1 -0
- package/dist/cjs/token-operation.js +3 -3
- package/dist/cjs/token-operation.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +58 -44
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +4 -0
- package/dist/operations.js.map +1 -1
- package/dist/sale-common.d.ts +1 -1
- package/dist/sale-common.d.ts.map +1 -1
- package/dist/sale-common.js +39 -4
- package/dist/sale-common.js.map +1 -1
- package/dist/snapshot-deployment.d.ts +83 -0
- package/dist/snapshot-deployment.d.ts.map +1 -0
- package/dist/snapshot-deployment.js +78 -0
- package/dist/snapshot-deployment.js.map +1 -0
- package/dist/snapshot-operation.d.ts +28 -0
- package/dist/snapshot-operation.d.ts.map +1 -0
- package/dist/snapshot-operation.js +2428 -0
- package/dist/snapshot-operation.js.map +1 -0
- package/dist/snapshot-signerless-transactions.d.ts +133 -0
- package/dist/snapshot-signerless-transactions.d.ts.map +1 -0
- package/dist/snapshot-signerless-transactions.js +735 -0
- package/dist/snapshot-signerless-transactions.js.map +1 -0
- package/dist/snapshot-signerless.d.ts +227 -0
- package/dist/snapshot-signerless.d.ts.map +1 -0
- package/dist/snapshot-signerless.js +993 -0
- package/dist/snapshot-signerless.js.map +1 -0
- package/dist/snapshot-verification.d.ts +58 -0
- package/dist/snapshot-verification.d.ts.map +1 -0
- package/dist/snapshot-verification.js +92 -0
- package/dist/snapshot-verification.js.map +1 -0
- package/dist/token-operation.js +3 -3
- package/dist/token-operation.js.map +1 -1
- package/docs/API.md +67 -0
- package/docs/COMPATIBILITY.json +84 -0
- package/docs/INTEGRATION.md +215 -0
- package/docs/LIVE_TRANSFER.md +106 -0
- package/docs/RECIPES.md +175 -0
- package/docs/TRANSACTION_GUIDE.md +163 -0
- package/docs/snapshot-start.md +27 -0
- package/examples/README.md +81 -0
- package/examples/backend/cli.ts +55 -0
- package/examples/backend/recipe.ts +39 -0
- package/examples/backend/runtime.ts +13 -0
- package/examples/frontend/index.html +48 -0
- package/examples/frontend/main.ts +213 -0
- package/examples/frontend/runtime.ts +14 -0
- package/examples/package-lock.json +1287 -0
- package/examples/package.json +23 -0
- package/examples/shared/build.ts +179 -0
- package/examples/shared/fixture.ts +24 -0
- package/examples/shared/live-transfer.ts +155 -0
- package/examples/shared/recipe-fixtures.ts +543 -0
- package/examples/shared/recipes.ts +55 -0
- package/examples/shared/signing.ts +110 -0
- package/examples/shared/timeout.ts +8 -0
- package/examples/test/browser.ts +116 -0
- package/examples/test/build.test.ts +197 -0
- package/examples/test/recipes.test.ts +59 -0
- package/examples/test/sources.test.ts +73 -0
- package/examples/tsconfig.json +26 -0
- package/examples/vendor/NOTICE.md +15 -0
- package/examples/vendor/kaspa/LICENSE +15 -0
- package/examples/vendor/kaspa/kaspa.d.ts +8908 -0
- package/examples/vendor/kaspa/kaspa.js +15202 -0
- package/examples/vendor/kaspa/kaspa_bg.wasm +0 -0
- package/examples/vendor/kaspa/package.json +24 -0
- package/package.json +21 -4
- package/src/artifacts.ts +35 -0
- package/src/engine.ts +91 -97
- package/src/index.ts +8 -0
- package/src/operations.ts +4 -0
- package/src/sale-common.ts +44 -3
- package/src/snapshot-deployment.ts +108 -0
- package/src/snapshot-operation.ts +3592 -0
- package/src/snapshot-signerless-transactions.ts +1272 -0
- package/src/snapshot-signerless.ts +1525 -0
- package/src/snapshot-verification.ts +86 -0
- package/src/token-operation.ts +3 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kcc20-tx-builder-examples",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"backend": "tsx backend/cli.ts",
|
|
7
|
+
"frontend": "vite frontend --host 127.0.0.1",
|
|
8
|
+
"build": "tsc --noEmit && vite build frontend",
|
|
9
|
+
"test": "tsx --test test/build.test.ts test/recipes.test.ts test/sources.test.ts",
|
|
10
|
+
"test:browser": "tsx test/browser.ts",
|
|
11
|
+
"recipe": "tsx backend/recipe.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@kaspacom/kcc20-tx-builder": "0.2.5"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "22.18.8",
|
|
18
|
+
"playwright": "1.56.0",
|
|
19
|
+
"tsx": "4.20.6",
|
|
20
|
+
"typescript": "5.9.3",
|
|
21
|
+
"vite": "6.4.4"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { withTimeout } from "./timeout.ts";
|
|
2
|
+
import { createKcc20PsktBuilderEngine } from "@kaspacom/kcc20-tx-builder/engine";
|
|
3
|
+
import { buildKcc20DeployTokenOperation } from "@kaspacom/kcc20-tx-builder/deploy-operation";
|
|
4
|
+
import { buildKcc20TransferTokenOperation } from "@kaspacom/kcc20-tx-builder/token-operation";
|
|
5
|
+
import {
|
|
6
|
+
assertKcc20ArtifactScriptHash,
|
|
7
|
+
isKcc20ArtifactKey,
|
|
8
|
+
} from "@kaspacom/kcc20-tx-builder/artifacts";
|
|
9
|
+
import type { Kcc20IndexedCovenantUtxo } from "@kaspacom/kcc20-tx-builder/action-source-resolver";
|
|
10
|
+
import type * as Wasm from "../vendor/kaspa/kaspa.js";
|
|
11
|
+
|
|
12
|
+
export const network = "testnet-10";
|
|
13
|
+
export type Runtime = typeof Wasm;
|
|
14
|
+
export type Wallet = { walletAddress: string; kcc20Owner: string };
|
|
15
|
+
export type Operation = ReturnType<typeof buildKcc20DeployTokenOperation>;
|
|
16
|
+
export type Sources = {
|
|
17
|
+
getUtxosByAddresses(args: { addresses: string[] }): Promise<unknown>;
|
|
18
|
+
};
|
|
19
|
+
export type ScriptArgument =
|
|
20
|
+
| { type: "i64"; value: string | number }
|
|
21
|
+
| { type: "data"; hex: string }
|
|
22
|
+
| { type: "byte"; value: number }
|
|
23
|
+
| { type: "signature"; prefixHex?: string };
|
|
24
|
+
export type ScriptHint = {
|
|
25
|
+
inputIndex: number;
|
|
26
|
+
scriptHex: string;
|
|
27
|
+
signType?: number;
|
|
28
|
+
signatureScript?: {
|
|
29
|
+
mode: "ordered-args" | "signature-first-args" | "wrap-signature";
|
|
30
|
+
args?: ScriptArgument[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export type Built = {
|
|
34
|
+
psktTransactionJson: string;
|
|
35
|
+
signInputs: { index: number; sighashType: number }[];
|
|
36
|
+
scripts?: ScriptHint[];
|
|
37
|
+
metadata: Record<string, any>;
|
|
38
|
+
submitTransactionSupported: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// This is application wiring, not a second SDK API.
|
|
42
|
+
export function engineInput(operation: Operation) {
|
|
43
|
+
const p = operation.payload;
|
|
44
|
+
if (p.network !== network)
|
|
45
|
+
throw new Error("These examples require testnet-10");
|
|
46
|
+
if (!p.owner.walletAddress.startsWith("kaspatest:"))
|
|
47
|
+
throw new Error("Expected a testnet wallet address");
|
|
48
|
+
return {
|
|
49
|
+
schema: "kcc20-in-process-pskt-builder-input/v1",
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
builder: {
|
|
52
|
+
builderKey: p.signing.builderKey,
|
|
53
|
+
operation: p.operation,
|
|
54
|
+
contract: p.contract,
|
|
55
|
+
action: p.action,
|
|
56
|
+
requiredSources: p.sourceRequirements,
|
|
57
|
+
},
|
|
58
|
+
request: {
|
|
59
|
+
builderKey: p.signing.builderKey,
|
|
60
|
+
operation: p.operation,
|
|
61
|
+
contract: p.contract,
|
|
62
|
+
action: p.action,
|
|
63
|
+
network: p.network,
|
|
64
|
+
params: p.params,
|
|
65
|
+
owner: p.owner,
|
|
66
|
+
requiredSources: p.sourceRequirements,
|
|
67
|
+
sourceRequirements: p.sourceRequirements,
|
|
68
|
+
signerRequirements: p.signerRequirements,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createBuilder(
|
|
74
|
+
wasm: Runtime,
|
|
75
|
+
sources: Sources,
|
|
76
|
+
loadArtifact: (key: string) => Promise<unknown>,
|
|
77
|
+
) {
|
|
78
|
+
for (const key of [
|
|
79
|
+
"Transaction",
|
|
80
|
+
"GenesisCovenantGroup",
|
|
81
|
+
"TransactionOutput",
|
|
82
|
+
"payToScriptHashScript",
|
|
83
|
+
] as const) {
|
|
84
|
+
if (typeof wasm[key] !== "function")
|
|
85
|
+
throw new Error(`Incompatible WASM runtime: missing ${key}`);
|
|
86
|
+
}
|
|
87
|
+
const engine = createKcc20PsktBuilderEngine({
|
|
88
|
+
wasm,
|
|
89
|
+
artifacts: {},
|
|
90
|
+
sourceProvider: {
|
|
91
|
+
getUtxosByAddresses: (args) =>
|
|
92
|
+
withTimeout(sources.getUtxosByAddresses(args), "UTXO query"),
|
|
93
|
+
},
|
|
94
|
+
config: { KASPA_NETWORK: network },
|
|
95
|
+
artifactProvider: async (key) => {
|
|
96
|
+
if (!isKcc20ArtifactKey(key))
|
|
97
|
+
throw new Error(`Unsupported artifact: ${key}`);
|
|
98
|
+
const artifact = await loadArtifact(key);
|
|
99
|
+
assertKcc20ArtifactScriptHash(key, artifact);
|
|
100
|
+
return artifact;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return async (operation: Operation): Promise<Built> => {
|
|
104
|
+
const result = await engine.build(engineInput(operation));
|
|
105
|
+
if (
|
|
106
|
+
typeof result.psktTransactionJson !== "string" ||
|
|
107
|
+
!Array.isArray(result.signInputs)
|
|
108
|
+
)
|
|
109
|
+
throw new Error("Invalid builder output");
|
|
110
|
+
return result as Built;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function deployOperation(wallet: Wallet) {
|
|
115
|
+
return buildKcc20DeployTokenOperation(
|
|
116
|
+
wallet,
|
|
117
|
+
{
|
|
118
|
+
ticker: "DEMO",
|
|
119
|
+
tokenName: "Example token",
|
|
120
|
+
decimals: 2,
|
|
121
|
+
maxSupply: "1000",
|
|
122
|
+
premintSupply: "1000",
|
|
123
|
+
mintPolicy: "fixed",
|
|
124
|
+
mintPricePerTokenSompi: "0",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
network,
|
|
128
|
+
requestId: "example-deploy",
|
|
129
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function transferOperation(
|
|
135
|
+
wallet: Wallet,
|
|
136
|
+
holder: Kcc20IndexedCovenantUtxo,
|
|
137
|
+
recipientOwner: string,
|
|
138
|
+
tokenAmount = "12.50",
|
|
139
|
+
) {
|
|
140
|
+
if (!holder.covenantId) throw new Error("A native covenant ID is required");
|
|
141
|
+
return buildKcc20TransferTokenOperation(
|
|
142
|
+
wallet,
|
|
143
|
+
{
|
|
144
|
+
token: { covenantId: holder.covenantId, ticker: "DEMO", decimals: 2 },
|
|
145
|
+
activeUtxos: [holder],
|
|
146
|
+
recipientOwner,
|
|
147
|
+
tokenAmount,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
network,
|
|
151
|
+
requestId: "example-transfer",
|
|
152
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function inspect(result: Built) {
|
|
158
|
+
const tx = JSON.parse(result.psktTransactionJson);
|
|
159
|
+
const inputSompi = tx.inputs.reduce(
|
|
160
|
+
(n: bigint, input: any) => n + BigInt(input.utxo.amount),
|
|
161
|
+
0n,
|
|
162
|
+
);
|
|
163
|
+
const outputSompi = tx.outputs.reduce(
|
|
164
|
+
(n: bigint, output: any) => n + BigInt(output.value),
|
|
165
|
+
0n,
|
|
166
|
+
);
|
|
167
|
+
return {
|
|
168
|
+
operation: result.metadata.builderKey,
|
|
169
|
+
network: result.metadata.network,
|
|
170
|
+
inputs: tx.inputs.length,
|
|
171
|
+
outputs: tx.outputs.length,
|
|
172
|
+
feeSompi: (inputSompi - outputSompi).toString(),
|
|
173
|
+
signInputs: result.signInputs,
|
|
174
|
+
intent:
|
|
175
|
+
result.metadata.intentOutputs?.map(
|
|
176
|
+
({ revealScriptHex, ...intent }: any) => intent,
|
|
177
|
+
) ?? null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { network, type Runtime, type Wallet, type Built, type Sources } from './build.ts';
|
|
2
|
+
import type { Kcc20IndexedCovenantUtxo } from '@kaspacom/kcc20-tx-builder/action-source-resolver';
|
|
3
|
+
|
|
4
|
+
// Public secp256k1 generator coordinates, not a wallet intended to receive funds.
|
|
5
|
+
export const owner = '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
|
|
6
|
+
export const recipient = 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5';
|
|
7
|
+
export function fixture(wasm: Runtime) {
|
|
8
|
+
const wallet: Wallet = { kcc20Owner: owner, walletAddress: new wasm.XOnlyPublicKey(owner).toAddress(network).toString() };
|
|
9
|
+
const rows = new Map<string, any[]>();
|
|
10
|
+
rows.set(wallet.walletAddress, [{ address: wallet.walletAddress, outpoint: { transactionId: 'ab'.repeat(32), index: 0 }, amount: 100_000_000_000n, scriptPublicKey: wasm.payToAddressScript(wallet.walletAddress), blockDaaScore: 1n, isCoinbase: false }]);
|
|
11
|
+
const sources: Sources = { async getUtxosByAddresses({ addresses }) { return { entries: addresses.flatMap(address => rows.get(address) ?? []) }; } };
|
|
12
|
+
function addDeploy(result: Built): Kcc20IndexedCovenantUtxo {
|
|
13
|
+
const tx = JSON.parse(result.psktTransactionJson);
|
|
14
|
+
const m = result.metadata;
|
|
15
|
+
const address = String(m.contractAddress);
|
|
16
|
+
const index = Number(m.covenantOutputIndex);
|
|
17
|
+
const output = tx.outputs[index];
|
|
18
|
+
const txid = 'cd'.repeat(32); // Synthetic, never submitted or asserted to exist on-chain.
|
|
19
|
+
rows.set(address, [{ address, outpoint: { transactionId: txid, index }, amount: BigInt(output.value), scriptPublicKey: output.scriptPublicKey, covenantId: m.covenantId, blockDaaScore: 1n, isCoinbase: false }]);
|
|
20
|
+
return { txidHex: txid, vout: index, outpoint: `${txid}:${index}`, address, amountSompi: output.value, covenantId: m.covenantId,
|
|
21
|
+
state: { ownerIdentifier: owner, ownerScheme: 0, amount: m.maxSupply, extensionCommitment: m.holderExtensionCommitment, borrowScheme: 0, borrowGuard: '00'.repeat(32), isMintAuthority: false } };
|
|
22
|
+
}
|
|
23
|
+
return { wallet, sources, rows, addDeploy };
|
|
24
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { buildKcc20ScriptForState } from "@kaspacom/kcc20-tx-builder/abi";
|
|
2
|
+
import { buildKcc20TransferTokenOperation } from "@kaspacom/kcc20-tx-builder/token-operation";
|
|
3
|
+
import {
|
|
4
|
+
normalizeKcc20IndexedUtxos,
|
|
5
|
+
selectOwnerNativeHolderUtxo,
|
|
6
|
+
} from "@kaspacom/kcc20-tx-builder/action-source-resolver";
|
|
7
|
+
import { parseKcc20DisplayAmountToBaseUnits } from "@kaspacom/kcc20-tx-builder/token-amount";
|
|
8
|
+
import { assertKcc20ArtifactScriptHash } from "@kaspacom/kcc20-tx-builder/artifacts";
|
|
9
|
+
import { network, type Runtime, type Sources, type Wallet } from "./build.ts";
|
|
10
|
+
import { withTimeout } from "./timeout.ts";
|
|
11
|
+
|
|
12
|
+
export interface LiveTransferConfig {
|
|
13
|
+
network: "testnet-10";
|
|
14
|
+
stateUrl: string;
|
|
15
|
+
wallet: Wallet;
|
|
16
|
+
covenantId: string;
|
|
17
|
+
decimals: number;
|
|
18
|
+
recipientOwner: string;
|
|
19
|
+
tokenAmount: string;
|
|
20
|
+
}
|
|
21
|
+
function bytes(value: unknown, field: string): Uint8Array {
|
|
22
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/i.test(value))
|
|
23
|
+
throw new Error(`Invalid ${field}`);
|
|
24
|
+
return Uint8Array.from(value.match(/../g)!, (x) => parseInt(x, 16));
|
|
25
|
+
}
|
|
26
|
+
// Endpoint response: { network: 'testnet-10', utxos: Kcc20IndexedCovenantUtxo[] }.
|
|
27
|
+
// It supplies decoded state; RPC independently confirms the current output.
|
|
28
|
+
export async function liveTransferOperation(
|
|
29
|
+
wasm: Runtime,
|
|
30
|
+
rpc: Sources,
|
|
31
|
+
loadArtifact: (key: string) => Promise<unknown>,
|
|
32
|
+
config: LiveTransferConfig,
|
|
33
|
+
fetcher: typeof fetch = fetch,
|
|
34
|
+
) {
|
|
35
|
+
if (config.network !== network)
|
|
36
|
+
throw new Error("Transfer config requires testnet-10");
|
|
37
|
+
if (
|
|
38
|
+
!Number.isInteger(config.decimals) ||
|
|
39
|
+
config.decimals < 0 ||
|
|
40
|
+
config.decimals > 8
|
|
41
|
+
)
|
|
42
|
+
throw new Error("Invalid decimals");
|
|
43
|
+
bytes(config.covenantId, "native covenant ID");
|
|
44
|
+
bytes(config.recipientOwner, "recipient owner");
|
|
45
|
+
bytes(config.wallet.kcc20Owner, "wallet owner");
|
|
46
|
+
if (
|
|
47
|
+
new wasm.XOnlyPublicKey(config.wallet.kcc20Owner)
|
|
48
|
+
.toAddress(network)
|
|
49
|
+
.toString() !== config.wallet.walletAddress
|
|
50
|
+
)
|
|
51
|
+
throw new Error("Wallet address and owner do not match");
|
|
52
|
+
const url = new URL(config.stateUrl);
|
|
53
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
54
|
+
throw new Error("State URL must use HTTP or HTTPS");
|
|
55
|
+
const document = await withTimeout(
|
|
56
|
+
(async () => {
|
|
57
|
+
const response = await fetcher(url, {
|
|
58
|
+
signal: AbortSignal.timeout(20_000),
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok)
|
|
61
|
+
throw new Error(`State endpoint returned HTTP ${response.status}`);
|
|
62
|
+
return await response.json();
|
|
63
|
+
})(),
|
|
64
|
+
"Decoded state query",
|
|
65
|
+
);
|
|
66
|
+
if (
|
|
67
|
+
document.network !== network ||
|
|
68
|
+
!Array.isArray(document.utxos) ||
|
|
69
|
+
document.utxos.length > 1000
|
|
70
|
+
)
|
|
71
|
+
throw new Error("Invalid or wrong-network state response");
|
|
72
|
+
const rows = normalizeKcc20IndexedUtxos(document.utxos);
|
|
73
|
+
if (rows.length !== document.utxos.length)
|
|
74
|
+
throw new Error("State response contains malformed or duplicate outputs");
|
|
75
|
+
const amount = parseKcc20DisplayAmountToBaseUnits(
|
|
76
|
+
config.tokenAmount,
|
|
77
|
+
config.decimals,
|
|
78
|
+
"tokenAmount",
|
|
79
|
+
);
|
|
80
|
+
const holder = selectOwnerNativeHolderUtxo(
|
|
81
|
+
rows.filter((row) => row.covenantId === config.covenantId),
|
|
82
|
+
config.wallet.kcc20Owner,
|
|
83
|
+
amount,
|
|
84
|
+
);
|
|
85
|
+
if (!holder?.state)
|
|
86
|
+
throw new Error("No matching owner holder can cover the transfer");
|
|
87
|
+
const state = holder.state;
|
|
88
|
+
if (
|
|
89
|
+
state.isMintAuthority !== false ||
|
|
90
|
+
state.ownerScheme !== 0 ||
|
|
91
|
+
state.borrowScheme !== 0
|
|
92
|
+
)
|
|
93
|
+
throw new Error("Expected an unborrowed Schnorr holder");
|
|
94
|
+
if (
|
|
95
|
+
typeof state.amount !== "string" ||
|
|
96
|
+
!/^[0-9]+$/.test(state.amount) ||
|
|
97
|
+
BigInt(String(state.amount)) <= 0n
|
|
98
|
+
)
|
|
99
|
+
throw new Error("Invalid holder token amount");
|
|
100
|
+
const artifact = await loadArtifact("KCC20.placeholder.json");
|
|
101
|
+
assertKcc20ArtifactScriptHash("KCC20.placeholder.json", artifact);
|
|
102
|
+
const script = buildKcc20ScriptForState(artifact, {
|
|
103
|
+
amount: BigInt(String(state.amount)),
|
|
104
|
+
owner: bytes(state.ownerIdentifier, "holder owner"),
|
|
105
|
+
ownerScheme: 0,
|
|
106
|
+
borrowScheme: 0,
|
|
107
|
+
borrowGuard: bytes(state.borrowGuard, "borrow guard"),
|
|
108
|
+
extensionCommitment: bytes(
|
|
109
|
+
state.extensionCommitment,
|
|
110
|
+
"extension commitment",
|
|
111
|
+
),
|
|
112
|
+
});
|
|
113
|
+
const expectedAddress = wasm
|
|
114
|
+
.addressFromScriptPublicKey(wasm.payToScriptHashScript(script), network)!
|
|
115
|
+
.toString();
|
|
116
|
+
if (holder.address !== expectedAddress)
|
|
117
|
+
throw new Error("Decoded state does not match the holder address");
|
|
118
|
+
const response = (await withTimeout(
|
|
119
|
+
rpc.getUtxosByAddresses({ addresses: [holder.address] }),
|
|
120
|
+
"Holder UTXO query",
|
|
121
|
+
)) as {
|
|
122
|
+
entries?: {
|
|
123
|
+
outpoint: { transactionId: string; index: number };
|
|
124
|
+
amount: bigint | string;
|
|
125
|
+
covenantId?: string;
|
|
126
|
+
scriptPublicKey: Parameters<Runtime["addressFromScriptPublicKey"]>[0];
|
|
127
|
+
}[];
|
|
128
|
+
};
|
|
129
|
+
const current = response.entries?.find(
|
|
130
|
+
(row) =>
|
|
131
|
+
String(row.outpoint.transactionId) === holder.txidHex &&
|
|
132
|
+
row.outpoint.index === holder.vout,
|
|
133
|
+
);
|
|
134
|
+
if (!current) throw new Error("Holder is spent or missing from RPC");
|
|
135
|
+
if (String(current.covenantId) !== config.covenantId)
|
|
136
|
+
throw new Error("Holder native covenant ID differs from RPC");
|
|
137
|
+
if (BigInt(current.amount) !== BigInt(holder.amountSompi))
|
|
138
|
+
throw new Error("Holder KAS amount differs from RPC");
|
|
139
|
+
if (
|
|
140
|
+
wasm
|
|
141
|
+
.addressFromScriptPublicKey(current.scriptPublicKey, network)
|
|
142
|
+
?.toString() !== expectedAddress
|
|
143
|
+
)
|
|
144
|
+
throw new Error("Holder script differs from RPC");
|
|
145
|
+
return buildKcc20TransferTokenOperation(
|
|
146
|
+
config.wallet,
|
|
147
|
+
{
|
|
148
|
+
token: { covenantId: config.covenantId, decimals: config.decimals },
|
|
149
|
+
activeUtxos: [holder],
|
|
150
|
+
recipientOwner: config.recipientOwner,
|
|
151
|
+
tokenAmount: config.tokenAmount,
|
|
152
|
+
},
|
|
153
|
+
{ network },
|
|
154
|
+
);
|
|
155
|
+
}
|