@totemsdk/omnia-vtxo 0.2.0 → 0.3.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/dist/anchor.d.ts +87 -0
- package/dist/anchor.js +107 -0
- package/dist/cjs/anchor.d.ts +87 -0
- package/dist/cjs/anchor.js +114 -0
- package/dist/cjs/commitment.d.ts +41 -0
- package/dist/cjs/commitment.js +173 -0
- package/dist/cjs/constants.d.ts +10 -0
- package/dist/cjs/constants.js +13 -0
- package/dist/cjs/durable-store.d.ts +33 -0
- package/dist/cjs/durable-store.js +93 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.js +77 -0
- package/dist/cjs/exit-tx.d.ts +55 -0
- package/dist/cjs/exit-tx.js +81 -0
- package/dist/cjs/exit.d.ts +29 -0
- package/dist/cjs/exit.js +115 -0
- package/dist/cjs/index.d.ts +21 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/memory-store.d.ts +17 -0
- package/dist/cjs/memory-store.js +51 -0
- package/dist/cjs/merge.d.ts +8 -0
- package/dist/cjs/merge.js +106 -0
- package/dist/cjs/operator.d.ts +29 -0
- package/dist/cjs/operator.js +65 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/policy.d.ts +22 -0
- package/dist/cjs/policy.js +27 -0
- package/dist/cjs/pool-script.d.ts +19 -0
- package/dist/cjs/pool-script.js +41 -0
- package/dist/cjs/pool.d.ts +5 -0
- package/dist/cjs/pool.js +60 -0
- package/dist/cjs/receipt.d.ts +31 -0
- package/dist/cjs/receipt.js +52 -0
- package/dist/cjs/refresh.d.ts +8 -0
- package/dist/cjs/refresh.js +84 -0
- package/dist/cjs/serialization.d.ts +5 -0
- package/dist/cjs/serialization.js +31 -0
- package/dist/cjs/split.d.ts +8 -0
- package/dist/cjs/split.js +96 -0
- package/dist/cjs/transfer.d.ts +8 -0
- package/dist/cjs/transfer.js +152 -0
- package/dist/cjs/types.d.ts +166 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/verify.d.ts +23 -0
- package/dist/cjs/verify.js +121 -0
- package/dist/cjs/vtxo.d.ts +14 -0
- package/dist/cjs/vtxo.js +96 -0
- package/dist/commitment.d.ts +13 -0
- package/dist/commitment.js +39 -30
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +4 -9
- package/dist/durable-store.d.ts +33 -0
- package/dist/durable-store.js +90 -0
- package/dist/errors.js +10 -23
- package/dist/exit-tx.d.ts +55 -0
- package/dist/exit-tx.js +74 -0
- package/dist/exit.d.ts +12 -2
- package/dist/exit.js +39 -23
- package/dist/index.d.ts +6 -0
- package/dist/index.js +21 -31
- package/dist/memory-store.js +3 -7
- package/dist/merge.js +15 -19
- package/dist/operator.d.ts +29 -0
- package/dist/operator.js +61 -0
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +3 -7
- package/dist/pool-script.d.ts +19 -0
- package/dist/pool-script.js +36 -0
- package/dist/pool.js +21 -27
- package/dist/receipt.d.ts +31 -0
- package/dist/receipt.js +48 -0
- package/dist/refresh.js +10 -14
- package/dist/serialization.js +4 -10
- package/dist/split.js +13 -17
- package/dist/transfer.js +13 -17
- package/dist/types.d.ts +12 -1
- package/dist/types.js +1 -2
- package/dist/verify.js +7 -13
- package/dist/vtxo.js +14 -19
- package/package.json +19 -8
- package/rust/pkg/omnia_vtxo_wasm_bg.wasm +0 -0
- package/rust/pkg/package.json +1 -1
- package/rust/pkg-node/omnia_vtxo_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +1 -1
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO batch on-chain anchoring (RFC-038 V1).
|
|
3
|
+
*
|
|
4
|
+
* Builds a batch-anchor transaction that (re)commits an epoch's Merkle root into
|
|
5
|
+
* the pool's on-chain state output, signs the tx digest, and — when a chain
|
|
6
|
+
* provider is supplied — mines and broadcasts the assembled TxPoW. The signing
|
|
7
|
+
* and mining are injected so the construction is testable without key material or
|
|
8
|
+
* real PoW.
|
|
9
|
+
*/
|
|
10
|
+
import { type OmniaTxDraft, type StateValue } from '@totemsdk/omnia';
|
|
11
|
+
import type { ChainStateProvider } from '@totemsdk/chain-provider';
|
|
12
|
+
export interface BatchAnchorParams {
|
|
13
|
+
operatorPublicKeyDigest: string;
|
|
14
|
+
/** The epoch's Merkle root (32-byte hex). */
|
|
15
|
+
root: string;
|
|
16
|
+
epoch: number;
|
|
17
|
+
/** The pool's current on-chain coin (input). */
|
|
18
|
+
fundingCoinId: string;
|
|
19
|
+
fundingAddress: string;
|
|
20
|
+
fundingScript: string;
|
|
21
|
+
/** Raw on-chain amount (token base units). */
|
|
22
|
+
amount: bigint;
|
|
23
|
+
tokenId: string;
|
|
24
|
+
}
|
|
25
|
+
/** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
|
|
26
|
+
export declare function poolStateVariables(root: string, epoch: number): StateValue[];
|
|
27
|
+
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
28
|
+
export declare function buildBatchAnchorDraft(params: BatchAnchorParams): OmniaTxDraft;
|
|
29
|
+
export declare function batchAnchorDigest(draft: OmniaTxDraft): Uint8Array;
|
|
30
|
+
interface MinedTxPow {
|
|
31
|
+
minedHeaderBytes: Uint8Array;
|
|
32
|
+
txpowId: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
export interface BatchAnchorDeps {
|
|
35
|
+
/** Sign the batch-anchor tx digest (e.g. via a leased WOTS signer). */
|
|
36
|
+
signDigest: (digest: Uint8Array) => Promise<Uint8Array>;
|
|
37
|
+
chainProvider?: ChainStateProvider;
|
|
38
|
+
/** PoW miner (defaults to `@totemsdk/txpow`'s mineTxPoW). */
|
|
39
|
+
mine?: (txBody: Uint8Array, difficulty: Uint8Array) => Promise<MinedTxPow>;
|
|
40
|
+
}
|
|
41
|
+
export interface BatchAnchorResult {
|
|
42
|
+
root: string;
|
|
43
|
+
epoch: number;
|
|
44
|
+
digest: string;
|
|
45
|
+
witnessHex: string;
|
|
46
|
+
txpowId?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Sign (and, when a chain provider is given, mine + broadcast) a batch anchor. */
|
|
49
|
+
export declare function anchorBatch(params: BatchAnchorParams, deps: BatchAnchorDeps): Promise<BatchAnchorResult>;
|
|
50
|
+
export interface PoolFundingExpectation {
|
|
51
|
+
address: string;
|
|
52
|
+
/** Raw base-unit amount, decimal string. */
|
|
53
|
+
amount: string;
|
|
54
|
+
tokenId: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PoolFundingCheck {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
coinId: string;
|
|
59
|
+
coin?: {
|
|
60
|
+
address: string;
|
|
61
|
+
amount: string;
|
|
62
|
+
tokenId: string;
|
|
63
|
+
spent: boolean;
|
|
64
|
+
};
|
|
65
|
+
/** The fetched MMR proof (present when `ok` and the node served one). */
|
|
66
|
+
proof?: unknown;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the MMR proof was cryptographically verified. `false` unless a
|
|
69
|
+
* `verifyProof` callback was supplied — this function checks existence,
|
|
70
|
+
* address/amount/token match, and unspent status, but does NOT itself verify the
|
|
71
|
+
* proof against the tip. Pass `verifyProof` to verify it (RFC-038 §3.3 open).
|
|
72
|
+
*/
|
|
73
|
+
proofVerified: boolean;
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check that the pool's on-chain coin exists, matches the expected
|
|
78
|
+
* (address, amount, token), is unspent, and that the node serves an MMR proof.
|
|
79
|
+
*
|
|
80
|
+
* NOTE: this is a **presence/shape check**, not a cryptographic proof
|
|
81
|
+
* verification, unless a `verifyProof` callback is supplied — see
|
|
82
|
+
* `PoolFundingCheck.proofVerified`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function checkPoolFunding(coinId: string, expected: PoolFundingExpectation, chainProvider: ChainStateProvider, options?: {
|
|
85
|
+
verifyProof?: (proof: unknown, coinId: string) => boolean;
|
|
86
|
+
}): Promise<PoolFundingCheck>;
|
|
87
|
+
export {};
|
package/dist/anchor.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO batch on-chain anchoring (RFC-038 V1).
|
|
3
|
+
*
|
|
4
|
+
* Builds a batch-anchor transaction that (re)commits an epoch's Merkle root into
|
|
5
|
+
* the pool's on-chain state output, signs the tx digest, and — when a chain
|
|
6
|
+
* provider is supplied — mines and broadcasts the assembled TxPoW. The signing
|
|
7
|
+
* and mining are injected so the construction is testable without key material or
|
|
8
|
+
* real PoW.
|
|
9
|
+
*/
|
|
10
|
+
import { computeOmniaTxDigest, omniaDraftToMinimaBytes, } from '@totemsdk/omnia';
|
|
11
|
+
import { serializeTxBody, mineTxPoW, TX_POW_MIN_DIFFICULTY } from '@totemsdk/txpow';
|
|
12
|
+
import { concatBytes } from '@totemsdk/core';
|
|
13
|
+
import { buildAndHashPoolScript } from './pool-script.js';
|
|
14
|
+
/** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
|
|
15
|
+
export function poolStateVariables(root, epoch) {
|
|
16
|
+
return [
|
|
17
|
+
{ port: 100, value: root, type: 'string' },
|
|
18
|
+
{ port: 101, value: BigInt(epoch), type: 'number' },
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
22
|
+
export function buildBatchAnchorDraft(params) {
|
|
23
|
+
const { script, address } = buildAndHashPoolScript(params.operatorPublicKeyDigest);
|
|
24
|
+
// The input must be the operator's own pool output, not a foreign script.
|
|
25
|
+
if (params.fundingScript !== script) {
|
|
26
|
+
throw new Error('anchorBatch: fundingScript does not match the operator pool script');
|
|
27
|
+
}
|
|
28
|
+
if (params.fundingAddress !== address) {
|
|
29
|
+
throw new Error('anchorBatch: fundingAddress does not match the operator pool address');
|
|
30
|
+
}
|
|
31
|
+
const stateVariables = poolStateVariables(params.root, params.epoch);
|
|
32
|
+
return {
|
|
33
|
+
type: 'settlement',
|
|
34
|
+
inputs: [{
|
|
35
|
+
coinId: params.fundingCoinId,
|
|
36
|
+
address: params.fundingAddress,
|
|
37
|
+
amount: params.amount,
|
|
38
|
+
tokenId: params.tokenId,
|
|
39
|
+
scriptHex: params.fundingScript,
|
|
40
|
+
}],
|
|
41
|
+
outputs: [{
|
|
42
|
+
address,
|
|
43
|
+
amount: params.amount,
|
|
44
|
+
tokenId: params.tokenId,
|
|
45
|
+
storeState: true,
|
|
46
|
+
stateVariables,
|
|
47
|
+
}],
|
|
48
|
+
storeState: true,
|
|
49
|
+
stateVariables,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function batchAnchorDigest(draft) {
|
|
53
|
+
return computeOmniaTxDigest(draft);
|
|
54
|
+
}
|
|
55
|
+
/** Sign (and, when a chain provider is given, mine + broadcast) a batch anchor. */
|
|
56
|
+
export async function anchorBatch(params, deps) {
|
|
57
|
+
const draft = buildBatchAnchorDraft(params);
|
|
58
|
+
const digest = batchAnchorDigest(draft);
|
|
59
|
+
const signature = await deps.signDigest(digest);
|
|
60
|
+
const result = {
|
|
61
|
+
root: params.root,
|
|
62
|
+
epoch: params.epoch,
|
|
63
|
+
digest: Buffer.from(digest).toString('hex'),
|
|
64
|
+
witnessHex: Buffer.from(signature).toString('hex'),
|
|
65
|
+
};
|
|
66
|
+
if (deps.chainProvider) {
|
|
67
|
+
const mine = deps.mine ?? ((txBody, difficulty) => mineTxPoW(txBody, difficulty));
|
|
68
|
+
const draftBytes = omniaDraftToMinimaBytes(draft);
|
|
69
|
+
const txBody = serializeTxBody(draftBytes, signature);
|
|
70
|
+
const mined = await mine(txBody, TX_POW_MIN_DIFFICULTY);
|
|
71
|
+
const fullTxPoW = concatBytes(concatBytes(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
|
|
72
|
+
await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
|
|
73
|
+
result.txpowId = Buffer.from(mined.txpowId).toString('hex');
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check that the pool's on-chain coin exists, matches the expected
|
|
79
|
+
* (address, amount, token), is unspent, and that the node serves an MMR proof.
|
|
80
|
+
*
|
|
81
|
+
* NOTE: this is a **presence/shape check**, not a cryptographic proof
|
|
82
|
+
* verification, unless a `verifyProof` callback is supplied — see
|
|
83
|
+
* `PoolFundingCheck.proofVerified`.
|
|
84
|
+
*/
|
|
85
|
+
export async function checkPoolFunding(coinId, expected, chainProvider, options = {}) {
|
|
86
|
+
const coin = await chainProvider.getCoin(coinId);
|
|
87
|
+
if (!coin)
|
|
88
|
+
return { ok: false, coinId, proofVerified: false, error: 'pool coin not found' };
|
|
89
|
+
if (coin.spent)
|
|
90
|
+
return { ok: false, coinId, proofVerified: false, error: 'pool coin is already spent' };
|
|
91
|
+
const matches = coin.address === expected.address &&
|
|
92
|
+
String(coin.amount) === expected.amount &&
|
|
93
|
+
(coin.tokenid ?? '0x00') === expected.tokenId;
|
|
94
|
+
if (!matches)
|
|
95
|
+
return { ok: false, coinId, proofVerified: false, error: 'pool coin does not match expected address/amount/token' };
|
|
96
|
+
const proof = await chainProvider.getProof(coinId).catch(() => null);
|
|
97
|
+
if (!proof)
|
|
98
|
+
return { ok: false, coinId, proofVerified: false, error: 'no MMR proof for pool coin' };
|
|
99
|
+
const proofVerified = options.verifyProof ? options.verifyProof(proof, coinId) : false;
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
coinId,
|
|
103
|
+
coin: { address: coin.address, amount: String(coin.amount), tokenId: coin.tokenid, spent: Boolean(coin.spent) },
|
|
104
|
+
proof,
|
|
105
|
+
proofVerified,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO batch on-chain anchoring (RFC-038 V1).
|
|
3
|
+
*
|
|
4
|
+
* Builds a batch-anchor transaction that (re)commits an epoch's Merkle root into
|
|
5
|
+
* the pool's on-chain state output, signs the tx digest, and — when a chain
|
|
6
|
+
* provider is supplied — mines and broadcasts the assembled TxPoW. The signing
|
|
7
|
+
* and mining are injected so the construction is testable without key material or
|
|
8
|
+
* real PoW.
|
|
9
|
+
*/
|
|
10
|
+
import { type OmniaTxDraft, type StateValue } from '@totemsdk/omnia';
|
|
11
|
+
import type { ChainStateProvider } from '@totemsdk/chain-provider';
|
|
12
|
+
export interface BatchAnchorParams {
|
|
13
|
+
operatorPublicKeyDigest: string;
|
|
14
|
+
/** The epoch's Merkle root (32-byte hex). */
|
|
15
|
+
root: string;
|
|
16
|
+
epoch: number;
|
|
17
|
+
/** The pool's current on-chain coin (input). */
|
|
18
|
+
fundingCoinId: string;
|
|
19
|
+
fundingAddress: string;
|
|
20
|
+
fundingScript: string;
|
|
21
|
+
/** Raw on-chain amount (token base units). */
|
|
22
|
+
amount: bigint;
|
|
23
|
+
tokenId: string;
|
|
24
|
+
}
|
|
25
|
+
/** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
|
|
26
|
+
export declare function poolStateVariables(root: string, epoch: number): StateValue[];
|
|
27
|
+
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
28
|
+
export declare function buildBatchAnchorDraft(params: BatchAnchorParams): OmniaTxDraft;
|
|
29
|
+
export declare function batchAnchorDigest(draft: OmniaTxDraft): Uint8Array;
|
|
30
|
+
interface MinedTxPow {
|
|
31
|
+
minedHeaderBytes: Uint8Array;
|
|
32
|
+
txpowId: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
export interface BatchAnchorDeps {
|
|
35
|
+
/** Sign the batch-anchor tx digest (e.g. via a leased WOTS signer). */
|
|
36
|
+
signDigest: (digest: Uint8Array) => Promise<Uint8Array>;
|
|
37
|
+
chainProvider?: ChainStateProvider;
|
|
38
|
+
/** PoW miner (defaults to `@totemsdk/txpow`'s mineTxPoW). */
|
|
39
|
+
mine?: (txBody: Uint8Array, difficulty: Uint8Array) => Promise<MinedTxPow>;
|
|
40
|
+
}
|
|
41
|
+
export interface BatchAnchorResult {
|
|
42
|
+
root: string;
|
|
43
|
+
epoch: number;
|
|
44
|
+
digest: string;
|
|
45
|
+
witnessHex: string;
|
|
46
|
+
txpowId?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Sign (and, when a chain provider is given, mine + broadcast) a batch anchor. */
|
|
49
|
+
export declare function anchorBatch(params: BatchAnchorParams, deps: BatchAnchorDeps): Promise<BatchAnchorResult>;
|
|
50
|
+
export interface PoolFundingExpectation {
|
|
51
|
+
address: string;
|
|
52
|
+
/** Raw base-unit amount, decimal string. */
|
|
53
|
+
amount: string;
|
|
54
|
+
tokenId: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PoolFundingCheck {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
coinId: string;
|
|
59
|
+
coin?: {
|
|
60
|
+
address: string;
|
|
61
|
+
amount: string;
|
|
62
|
+
tokenId: string;
|
|
63
|
+
spent: boolean;
|
|
64
|
+
};
|
|
65
|
+
/** The fetched MMR proof (present when `ok` and the node served one). */
|
|
66
|
+
proof?: unknown;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the MMR proof was cryptographically verified. `false` unless a
|
|
69
|
+
* `verifyProof` callback was supplied — this function checks existence,
|
|
70
|
+
* address/amount/token match, and unspent status, but does NOT itself verify the
|
|
71
|
+
* proof against the tip. Pass `verifyProof` to verify it (RFC-038 §3.3 open).
|
|
72
|
+
*/
|
|
73
|
+
proofVerified: boolean;
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check that the pool's on-chain coin exists, matches the expected
|
|
78
|
+
* (address, amount, token), is unspent, and that the node serves an MMR proof.
|
|
79
|
+
*
|
|
80
|
+
* NOTE: this is a **presence/shape check**, not a cryptographic proof
|
|
81
|
+
* verification, unless a `verifyProof` callback is supplied — see
|
|
82
|
+
* `PoolFundingCheck.proofVerified`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function checkPoolFunding(coinId: string, expected: PoolFundingExpectation, chainProvider: ChainStateProvider, options?: {
|
|
85
|
+
verifyProof?: (proof: unknown, coinId: string) => boolean;
|
|
86
|
+
}): Promise<PoolFundingCheck>;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* VTXO batch on-chain anchoring (RFC-038 V1).
|
|
4
|
+
*
|
|
5
|
+
* Builds a batch-anchor transaction that (re)commits an epoch's Merkle root into
|
|
6
|
+
* the pool's on-chain state output, signs the tx digest, and — when a chain
|
|
7
|
+
* provider is supplied — mines and broadcasts the assembled TxPoW. The signing
|
|
8
|
+
* and mining are injected so the construction is testable without key material or
|
|
9
|
+
* real PoW.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.poolStateVariables = poolStateVariables;
|
|
13
|
+
exports.buildBatchAnchorDraft = buildBatchAnchorDraft;
|
|
14
|
+
exports.batchAnchorDigest = batchAnchorDigest;
|
|
15
|
+
exports.anchorBatch = anchorBatch;
|
|
16
|
+
exports.checkPoolFunding = checkPoolFunding;
|
|
17
|
+
const omnia_1 = require("@totemsdk/omnia");
|
|
18
|
+
const txpow_1 = require("@totemsdk/txpow");
|
|
19
|
+
const core_1 = require("@totemsdk/core");
|
|
20
|
+
const pool_script_js_1 = require("./pool-script.js");
|
|
21
|
+
/** State variables carried by the pool output: STATE(100)=root, STATE(101)=epoch. */
|
|
22
|
+
function poolStateVariables(root, epoch) {
|
|
23
|
+
return [
|
|
24
|
+
{ port: 100, value: root, type: 'string' },
|
|
25
|
+
{ port: 101, value: BigInt(epoch), type: 'number' },
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
29
|
+
function buildBatchAnchorDraft(params) {
|
|
30
|
+
const { script, address } = (0, pool_script_js_1.buildAndHashPoolScript)(params.operatorPublicKeyDigest);
|
|
31
|
+
// The input must be the operator's own pool output, not a foreign script.
|
|
32
|
+
if (params.fundingScript !== script) {
|
|
33
|
+
throw new Error('anchorBatch: fundingScript does not match the operator pool script');
|
|
34
|
+
}
|
|
35
|
+
if (params.fundingAddress !== address) {
|
|
36
|
+
throw new Error('anchorBatch: fundingAddress does not match the operator pool address');
|
|
37
|
+
}
|
|
38
|
+
const stateVariables = poolStateVariables(params.root, params.epoch);
|
|
39
|
+
return {
|
|
40
|
+
type: 'settlement',
|
|
41
|
+
inputs: [{
|
|
42
|
+
coinId: params.fundingCoinId,
|
|
43
|
+
address: params.fundingAddress,
|
|
44
|
+
amount: params.amount,
|
|
45
|
+
tokenId: params.tokenId,
|
|
46
|
+
scriptHex: params.fundingScript,
|
|
47
|
+
}],
|
|
48
|
+
outputs: [{
|
|
49
|
+
address,
|
|
50
|
+
amount: params.amount,
|
|
51
|
+
tokenId: params.tokenId,
|
|
52
|
+
storeState: true,
|
|
53
|
+
stateVariables,
|
|
54
|
+
}],
|
|
55
|
+
storeState: true,
|
|
56
|
+
stateVariables,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function batchAnchorDigest(draft) {
|
|
60
|
+
return (0, omnia_1.computeOmniaTxDigest)(draft);
|
|
61
|
+
}
|
|
62
|
+
/** Sign (and, when a chain provider is given, mine + broadcast) a batch anchor. */
|
|
63
|
+
async function anchorBatch(params, deps) {
|
|
64
|
+
const draft = buildBatchAnchorDraft(params);
|
|
65
|
+
const digest = batchAnchorDigest(draft);
|
|
66
|
+
const signature = await deps.signDigest(digest);
|
|
67
|
+
const result = {
|
|
68
|
+
root: params.root,
|
|
69
|
+
epoch: params.epoch,
|
|
70
|
+
digest: Buffer.from(digest).toString('hex'),
|
|
71
|
+
witnessHex: Buffer.from(signature).toString('hex'),
|
|
72
|
+
};
|
|
73
|
+
if (deps.chainProvider) {
|
|
74
|
+
const mine = deps.mine ?? ((txBody, difficulty) => (0, txpow_1.mineTxPoW)(txBody, difficulty));
|
|
75
|
+
const draftBytes = (0, omnia_1.omniaDraftToMinimaBytes)(draft);
|
|
76
|
+
const txBody = (0, txpow_1.serializeTxBody)(draftBytes, signature);
|
|
77
|
+
const mined = await mine(txBody, txpow_1.TX_POW_MIN_DIFFICULTY);
|
|
78
|
+
const fullTxPoW = (0, core_1.concatBytes)((0, core_1.concatBytes)(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
|
|
79
|
+
await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
|
|
80
|
+
result.txpowId = Buffer.from(mined.txpowId).toString('hex');
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check that the pool's on-chain coin exists, matches the expected
|
|
86
|
+
* (address, amount, token), is unspent, and that the node serves an MMR proof.
|
|
87
|
+
*
|
|
88
|
+
* NOTE: this is a **presence/shape check**, not a cryptographic proof
|
|
89
|
+
* verification, unless a `verifyProof` callback is supplied — see
|
|
90
|
+
* `PoolFundingCheck.proofVerified`.
|
|
91
|
+
*/
|
|
92
|
+
async function checkPoolFunding(coinId, expected, chainProvider, options = {}) {
|
|
93
|
+
const coin = await chainProvider.getCoin(coinId);
|
|
94
|
+
if (!coin)
|
|
95
|
+
return { ok: false, coinId, proofVerified: false, error: 'pool coin not found' };
|
|
96
|
+
if (coin.spent)
|
|
97
|
+
return { ok: false, coinId, proofVerified: false, error: 'pool coin is already spent' };
|
|
98
|
+
const matches = coin.address === expected.address &&
|
|
99
|
+
String(coin.amount) === expected.amount &&
|
|
100
|
+
(coin.tokenid ?? '0x00') === expected.tokenId;
|
|
101
|
+
if (!matches)
|
|
102
|
+
return { ok: false, coinId, proofVerified: false, error: 'pool coin does not match expected address/amount/token' };
|
|
103
|
+
const proof = await chainProvider.getProof(coinId).catch(() => null);
|
|
104
|
+
if (!proof)
|
|
105
|
+
return { ok: false, coinId, proofVerified: false, error: 'no MMR proof for pool coin' };
|
|
106
|
+
const proofVerified = options.verifyProof ? options.verifyProof(proof, coinId) : false;
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
coinId,
|
|
110
|
+
coin: { address: coin.address, amount: String(coin.amount), tokenId: coin.tokenid, spent: Boolean(coin.spent) },
|
|
111
|
+
proof,
|
|
112
|
+
proofVerified,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { OmniaVtxo, VtxoId, VtxoProof } from './types.js';
|
|
2
|
+
/** Domain-separated hash prefixes (#33) — a hash on one record cannot replay against another. */
|
|
3
|
+
export declare const VTXO_ID_DOMAIN = "totemsdk/omnia-vtxo/vtxo/v1";
|
|
4
|
+
export declare const VTXO_POOL_ID_DOMAIN = "totemsdk/omnia-vtxo/pool/v1";
|
|
5
|
+
export declare const VTXO_LEAF_DOMAIN = "totemsdk/omnia-vtxo/leaf/v1";
|
|
6
|
+
export declare const VTXO_RECEIPT_DOMAIN = "totemsdk/omnia-vtxo/receipt/v1";
|
|
7
|
+
export declare const VTXO_BATCH_DOMAIN = "totemsdk/omnia-vtxo/batch/v1";
|
|
8
|
+
/**
|
|
9
|
+
* Canonical batch commitment (RFC-038 §3.2): the operator signs this over the
|
|
10
|
+
* pool, epoch, Merkle root, and the ordered VTXO ids in the batch.
|
|
11
|
+
*/
|
|
12
|
+
export declare function computeBatchCommitment(poolId: string, epoch: number, root: string, orderedIds: readonly string[]): string;
|
|
13
|
+
/** The 32-byte digest of a batch commitment, for signing. */
|
|
14
|
+
export declare function computeBatchCommitmentBytes(poolId: string, epoch: number, root: string, orderedIds: readonly string[]): Uint8Array;
|
|
15
|
+
export declare function computeVtxoLeaf(vtxo: OmniaVtxo): string;
|
|
16
|
+
export interface ComputeVtxoIdParams {
|
|
17
|
+
poolId: string;
|
|
18
|
+
owner: string;
|
|
19
|
+
amount: bigint;
|
|
20
|
+
tokenId: string;
|
|
21
|
+
nonce: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function computeVtxoId(params: ComputeVtxoIdParams): string;
|
|
24
|
+
export interface ComputePoolIdParams {
|
|
25
|
+
operator: string;
|
|
26
|
+
tokenId: string;
|
|
27
|
+
nonce: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function computePoolId(params: ComputePoolIdParams): string;
|
|
30
|
+
export declare function computeCommitmentRoot(leaves: string[]): string;
|
|
31
|
+
export interface MerkleProofNode {
|
|
32
|
+
sibling: string;
|
|
33
|
+
position: 'left' | 'right';
|
|
34
|
+
}
|
|
35
|
+
export declare function verifyMerkleProof(leaf: string, proof: MerkleProofNode[], root: string): boolean;
|
|
36
|
+
export interface VtxoProofSet {
|
|
37
|
+
root: string;
|
|
38
|
+
proofs: Record<VtxoId, VtxoProof>;
|
|
39
|
+
}
|
|
40
|
+
export declare function buildVtxoProofSet(vtxos: OmniaVtxo[]): VtxoProofSet;
|
|
41
|
+
export declare function computeReceiptId(poolId: string, op: string, inputIds: string[], outputIds: string[], at: number): string;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VTXO_BATCH_DOMAIN = exports.VTXO_RECEIPT_DOMAIN = exports.VTXO_LEAF_DOMAIN = exports.VTXO_POOL_ID_DOMAIN = exports.VTXO_ID_DOMAIN = void 0;
|
|
4
|
+
exports.computeBatchCommitment = computeBatchCommitment;
|
|
5
|
+
exports.computeBatchCommitmentBytes = computeBatchCommitmentBytes;
|
|
6
|
+
exports.computeVtxoLeaf = computeVtxoLeaf;
|
|
7
|
+
exports.computeVtxoId = computeVtxoId;
|
|
8
|
+
exports.computePoolId = computePoolId;
|
|
9
|
+
exports.computeCommitmentRoot = computeCommitmentRoot;
|
|
10
|
+
exports.verifyMerkleProof = verifyMerkleProof;
|
|
11
|
+
exports.buildVtxoProofSet = buildVtxoProofSet;
|
|
12
|
+
exports.computeReceiptId = computeReceiptId;
|
|
13
|
+
const core_1 = require("@totemsdk/core");
|
|
14
|
+
const constants_js_1 = require("./constants.js");
|
|
15
|
+
/** Domain-separated hash prefixes (#33) — a hash on one record cannot replay against another. */
|
|
16
|
+
exports.VTXO_ID_DOMAIN = 'totemsdk/omnia-vtxo/vtxo/v1';
|
|
17
|
+
exports.VTXO_POOL_ID_DOMAIN = 'totemsdk/omnia-vtxo/pool/v1';
|
|
18
|
+
exports.VTXO_LEAF_DOMAIN = 'totemsdk/omnia-vtxo/leaf/v1';
|
|
19
|
+
exports.VTXO_RECEIPT_DOMAIN = 'totemsdk/omnia-vtxo/receipt/v1';
|
|
20
|
+
exports.VTXO_BATCH_DOMAIN = 'totemsdk/omnia-vtxo/batch/v1';
|
|
21
|
+
/**
|
|
22
|
+
* Canonical batch commitment (RFC-038 §3.2): the operator signs this over the
|
|
23
|
+
* pool, epoch, Merkle root, and the ordered VTXO ids in the batch.
|
|
24
|
+
*/
|
|
25
|
+
function computeBatchCommitment(poolId, epoch, root, orderedIds) {
|
|
26
|
+
return sha3hex(`${exports.VTXO_BATCH_DOMAIN}|${poolId}|${epoch}|${root}|${[...orderedIds].join(',')}`);
|
|
27
|
+
}
|
|
28
|
+
/** The 32-byte digest of a batch commitment, for signing. */
|
|
29
|
+
function computeBatchCommitmentBytes(poolId, epoch, root, orderedIds) {
|
|
30
|
+
return (0, core_1.hexToBytes)(computeBatchCommitment(poolId, epoch, root, orderedIds));
|
|
31
|
+
}
|
|
32
|
+
function sha3hex(input) {
|
|
33
|
+
const bytes = new TextEncoder().encode(input);
|
|
34
|
+
return (0, core_1.bytesToHex)((0, core_1.sha3_256)(bytes));
|
|
35
|
+
}
|
|
36
|
+
function hashPair(a, b) {
|
|
37
|
+
const [left, right] = a <= b ? [a, b] : [b, a];
|
|
38
|
+
const combined = left + right;
|
|
39
|
+
const bytes = (0, core_1.hexToBytes)(combined);
|
|
40
|
+
return (0, core_1.bytesToHex)((0, core_1.sha3_256)(bytes));
|
|
41
|
+
}
|
|
42
|
+
function computeVtxoLeaf(vtxo) {
|
|
43
|
+
const input = [
|
|
44
|
+
exports.VTXO_LEAF_DOMAIN,
|
|
45
|
+
vtxo.vtxoId,
|
|
46
|
+
vtxo.owner,
|
|
47
|
+
vtxo.amount.toString(),
|
|
48
|
+
vtxo.tokenId,
|
|
49
|
+
vtxo.epoch.toString(),
|
|
50
|
+
vtxo.poolId,
|
|
51
|
+
].join(':');
|
|
52
|
+
return sha3hex(input);
|
|
53
|
+
}
|
|
54
|
+
function computeVtxoId(params) {
|
|
55
|
+
const input = [
|
|
56
|
+
exports.VTXO_ID_DOMAIN,
|
|
57
|
+
params.poolId,
|
|
58
|
+
params.owner,
|
|
59
|
+
params.amount.toString(),
|
|
60
|
+
params.tokenId,
|
|
61
|
+
params.nonce,
|
|
62
|
+
].join(':');
|
|
63
|
+
return sha3hex(input);
|
|
64
|
+
}
|
|
65
|
+
function computePoolId(params) {
|
|
66
|
+
const input = [
|
|
67
|
+
exports.VTXO_POOL_ID_DOMAIN,
|
|
68
|
+
params.operator,
|
|
69
|
+
params.tokenId,
|
|
70
|
+
params.nonce,
|
|
71
|
+
].join(':');
|
|
72
|
+
return sha3hex(input);
|
|
73
|
+
}
|
|
74
|
+
function nextPowerOfTwo(n) {
|
|
75
|
+
if (n <= 1)
|
|
76
|
+
return 1;
|
|
77
|
+
let p = 1;
|
|
78
|
+
while (p < n)
|
|
79
|
+
p <<= 1;
|
|
80
|
+
return p;
|
|
81
|
+
}
|
|
82
|
+
function computeCommitmentRoot(leaves) {
|
|
83
|
+
if (leaves.length === 0)
|
|
84
|
+
return constants_js_1.EMPTY_LEAF;
|
|
85
|
+
const size = nextPowerOfTwo(leaves.length);
|
|
86
|
+
const level = new Array(size).fill(constants_js_1.EMPTY_LEAF);
|
|
87
|
+
for (let i = 0; i < leaves.length; i++) {
|
|
88
|
+
level[i] = leaves[i];
|
|
89
|
+
}
|
|
90
|
+
let current = level;
|
|
91
|
+
while (current.length > 1) {
|
|
92
|
+
const next = [];
|
|
93
|
+
for (let i = 0; i < current.length; i += 2) {
|
|
94
|
+
next.push(hashPair(current[i], current[i + 1]));
|
|
95
|
+
}
|
|
96
|
+
current = next;
|
|
97
|
+
}
|
|
98
|
+
return current[0];
|
|
99
|
+
}
|
|
100
|
+
function verifyMerkleProof(leaf, proof, root) {
|
|
101
|
+
let current = leaf;
|
|
102
|
+
for (const node of proof) {
|
|
103
|
+
if (node.position === 'left') {
|
|
104
|
+
current = hashPair(node.sibling, current);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
current = hashPair(current, node.sibling);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return current === root;
|
|
111
|
+
}
|
|
112
|
+
function buildVtxoProofSet(vtxos) {
|
|
113
|
+
if (vtxos.length === 0) {
|
|
114
|
+
return { root: constants_js_1.EMPTY_LEAF, proofs: {} };
|
|
115
|
+
}
|
|
116
|
+
const sorted = [...vtxos].sort((a, b) => a.vtxoId < b.vtxoId ? -1 : a.vtxoId > b.vtxoId ? 1 : 0);
|
|
117
|
+
const leafMap = new Map();
|
|
118
|
+
const leaves = sorted.map(v => {
|
|
119
|
+
const leaf = computeVtxoLeaf(v);
|
|
120
|
+
leafMap.set(v.vtxoId, leaf);
|
|
121
|
+
return leaf;
|
|
122
|
+
});
|
|
123
|
+
const size = nextPowerOfTwo(leaves.length);
|
|
124
|
+
const paddedLeaves = [...leaves];
|
|
125
|
+
while (paddedLeaves.length < size)
|
|
126
|
+
paddedLeaves.push(constants_js_1.EMPTY_LEAF);
|
|
127
|
+
const levels = [paddedLeaves];
|
|
128
|
+
let current = paddedLeaves;
|
|
129
|
+
while (current.length > 1) {
|
|
130
|
+
const next = [];
|
|
131
|
+
for (let i = 0; i < current.length; i += 2) {
|
|
132
|
+
next.push(hashPair(current[i], current[i + 1]));
|
|
133
|
+
}
|
|
134
|
+
levels.push(next);
|
|
135
|
+
current = next;
|
|
136
|
+
}
|
|
137
|
+
const root = current[0];
|
|
138
|
+
const proofs = {};
|
|
139
|
+
sorted.forEach((vtxo, idx) => {
|
|
140
|
+
const siblings = [];
|
|
141
|
+
const positions = [];
|
|
142
|
+
let pos = idx;
|
|
143
|
+
for (let lvl = 0; lvl < levels.length - 1; lvl++) {
|
|
144
|
+
const isLeft = pos % 2 === 0;
|
|
145
|
+
const siblingIdx = isLeft ? pos + 1 : pos - 1;
|
|
146
|
+
const sibling = levels[lvl][siblingIdx] ?? constants_js_1.EMPTY_LEAF;
|
|
147
|
+
siblings.push(sibling);
|
|
148
|
+
positions.push(isLeft ? 'right' : 'left');
|
|
149
|
+
pos = Math.floor(pos / 2);
|
|
150
|
+
}
|
|
151
|
+
const leaf = leafMap.get(vtxo.vtxoId);
|
|
152
|
+
proofs[vtxo.vtxoId] = {
|
|
153
|
+
leaf,
|
|
154
|
+
root,
|
|
155
|
+
siblings,
|
|
156
|
+
positions,
|
|
157
|
+
epoch: vtxo.epoch,
|
|
158
|
+
batchId: root,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
return { root, proofs };
|
|
162
|
+
}
|
|
163
|
+
function computeReceiptId(poolId, op, inputIds, outputIds, at) {
|
|
164
|
+
const input = [
|
|
165
|
+
exports.VTXO_RECEIPT_DOMAIN,
|
|
166
|
+
poolId,
|
|
167
|
+
op,
|
|
168
|
+
inputIds.sort().join(','),
|
|
169
|
+
outputIds.sort().join(','),
|
|
170
|
+
at.toString(),
|
|
171
|
+
].join(':');
|
|
172
|
+
return sha3hex(input);
|
|
173
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const EMPTY_LEAF = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
2
|
+
export declare const EMPTY_TREE_ROOT = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
3
|
+
export declare const EPOCH_ZERO = 0;
|
|
4
|
+
export declare const DEFAULT_POLICY: {
|
|
5
|
+
readonly minAmount: bigint;
|
|
6
|
+
readonly maxAmount: bigint;
|
|
7
|
+
readonly maxMergeInputs: 8;
|
|
8
|
+
readonly maxSplitOutputs: 8;
|
|
9
|
+
readonly exitTimelockSeconds: 86400;
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_POLICY = exports.EPOCH_ZERO = exports.EMPTY_TREE_ROOT = exports.EMPTY_LEAF = void 0;
|
|
4
|
+
exports.EMPTY_LEAF = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
5
|
+
exports.EMPTY_TREE_ROOT = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
6
|
+
exports.EPOCH_ZERO = 0;
|
|
7
|
+
exports.DEFAULT_POLICY = {
|
|
8
|
+
minAmount: BigInt(1),
|
|
9
|
+
maxAmount: BigInt(1000000000000000),
|
|
10
|
+
maxMergeInputs: 8,
|
|
11
|
+
maxSplitOutputs: 8,
|
|
12
|
+
exitTimelockSeconds: 86400,
|
|
13
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable VTXO/pool snapshot store (RFC-007 G6).
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for `MemoryOmniaVtxoStore` — same `OmniaVtxoStore` port
|
|
5
|
+
* — backed by a storage adapter instead of heap maps: the whole pool/VTXO
|
|
6
|
+
* registry is a single revision-CAS snapshot record, so concurrent pool/VTXO
|
|
7
|
+
* transitions never lose an update, reopen restores every VTXO (incl. spent
|
|
8
|
+
* status and history), and corruption is surfaced — never treated as absence.
|
|
9
|
+
*/
|
|
10
|
+
import type { StorageAdapterWithCapabilities, CasStore, WriteAckMode } from '@totemsdk/storage/types';
|
|
11
|
+
import type { OmniaVtxo, OmniaVtxoPool, OmniaVtxoStore } from './types.js';
|
|
12
|
+
export interface OmniaVtxoRegistryState {
|
|
13
|
+
pools: Record<string, OmniaVtxoPool>;
|
|
14
|
+
vtxos: Record<string, OmniaVtxo>;
|
|
15
|
+
}
|
|
16
|
+
export interface DurableOmniaVtxoStoreOptions {
|
|
17
|
+
/** Key namespace prefix; default `totem_omnia_vtxo:v1:`. */
|
|
18
|
+
readonly namespace?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Required write acknowledgment; default `durably-acknowledged`. Pass
|
|
21
|
+
* `volatile` only for tests/scratch adapters (e.g. `MemoryStore`).
|
|
22
|
+
*/
|
|
23
|
+
readonly requireAckMode?: WriteAckMode;
|
|
24
|
+
}
|
|
25
|
+
export interface DurableOmniaVtxoStore extends OmniaVtxoStore {
|
|
26
|
+
/** Current registry transition counter (0 before the first write). */
|
|
27
|
+
getRevision(): Promise<number>;
|
|
28
|
+
/** True once any snapshot record has been persisted. */
|
|
29
|
+
hasState(): Promise<boolean>;
|
|
30
|
+
/** Current persisted snapshot state (pool + vtxo maps). */
|
|
31
|
+
getSnapshot(): Promise<OmniaVtxoRegistryState>;
|
|
32
|
+
}
|
|
33
|
+
export declare function createDurableOmniaVtxoStore(adapter: StorageAdapterWithCapabilities & CasStore, options?: DurableOmniaVtxoStoreOptions): DurableOmniaVtxoStore;
|