@totemsdk/omnia-vtxo 0.4.0 → 0.5.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 +8 -0
- package/dist/anchor.d.ts +7 -4
- package/dist/anchor.js +9 -5
- package/dist/cjs/anchor.d.ts +7 -4
- package/dist/cjs/anchor.js +8 -4
- package/dist/cjs/exit-tx.d.ts +23 -18
- package/dist/cjs/exit-tx.js +40 -27
- package/dist/cjs/pool-script.d.ts +22 -17
- package/dist/cjs/pool-script.js +54 -24
- package/dist/exit-tx.d.ts +23 -18
- package/dist/exit-tx.js +40 -27
- package/dist/pool-script.d.ts +22 -17
- package/dist/pool-script.js +53 -24
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Virtual UTXO (vTXO) / payment-pool claim layer for the Totem Edge ecosystem.
|
|
4
4
|
|
|
5
|
+
> **Role:** the **spendable balances**. A VTXO is a cash-like off-chain bearer
|
|
6
|
+
> claim on a pool's on-chain capacity. This package owns the pool output script,
|
|
7
|
+
> the VTXO lifecycle (mint / transfer / split / merge / refresh / exit), the
|
|
8
|
+
> operator co-signature, and the per-share exit. It is **not** the LP accounting
|
|
9
|
+
> layer — that is `@totemsdk/liquidity-bond` + `@totemsdk/omnia-pool`, which
|
|
10
|
+
> allocate capital *into* a VTXO pool. They compose; they don't overlap.
|
|
11
|
+
> See [`docs/examples/meridian-vtxo-payments.md`](../../docs/examples/meridian-vtxo-payments.md).
|
|
12
|
+
|
|
5
13
|
## What is Omnia VTXO?
|
|
6
14
|
|
|
7
15
|
`@totemsdk/omnia-vtxo` is a **cash-like off-chain balance primitive** that gives Totem Edge applications a way to hold, transfer, split, merge, and exit token balances without requiring a live Minima node for every operation. Each VTXO (Virtual Transaction Output) is a claim on a pool of on-chain capacity, backed by a deterministic Merkle commitment tree.
|
package/dist/anchor.d.ts
CHANGED
|
@@ -11,8 +11,11 @@ import { type OmniaTxDraft, type StateValue } from '@totemsdk/omnia';
|
|
|
11
11
|
import type { ChainStateProvider } from '@totemsdk/chain-provider';
|
|
12
12
|
export interface BatchAnchorParams {
|
|
13
13
|
operatorPublicKeyDigest: string;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/** Holder WOTS public-key digests (one per pool share). */
|
|
15
|
+
holders: string[];
|
|
16
|
+
/** Committed share per holder (aligned with `holders`); Σ = pool capacity. */
|
|
17
|
+
shares: bigint[];
|
|
18
|
+
/** Absolute block height after which holders may exit unilaterally (STATE(102)). */
|
|
16
19
|
exitBlock: number;
|
|
17
20
|
/** The epoch's Merkle root (32-byte hex). */
|
|
18
21
|
root: string;
|
|
@@ -25,8 +28,8 @@ export interface BatchAnchorParams {
|
|
|
25
28
|
amount: bigint;
|
|
26
29
|
tokenId: string;
|
|
27
30
|
}
|
|
28
|
-
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
|
|
29
|
-
export declare function poolStateVariables(root: string, epoch: number, exitBlock: number): StateValue[];
|
|
31
|
+
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock, STATE(300+k)=share k. */
|
|
32
|
+
export declare function poolStateVariables(root: string, epoch: number, exitBlock: number, shares?: bigint[]): StateValue[];
|
|
30
33
|
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
31
34
|
export declare function buildBatchAnchorDraft(params: BatchAnchorParams): OmniaTxDraft;
|
|
32
35
|
export declare function batchAnchorDigest(draft: OmniaTxDraft): Uint8Array;
|
package/dist/anchor.js
CHANGED
|
@@ -10,20 +10,24 @@
|
|
|
10
10
|
import { computeOmniaTxDigest, omniaDraftToMinimaBytes, } from '@totemsdk/omnia';
|
|
11
11
|
import { serializeTxBody, mineTxPoW, TX_POW_MIN_DIFFICULTY } from '@totemsdk/txpow';
|
|
12
12
|
import { concatBytes } from '@totemsdk/core';
|
|
13
|
-
import { buildAndHashPoolScript } from './pool-script.js';
|
|
14
|
-
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
|
|
15
|
-
export function poolStateVariables(root, epoch, exitBlock) {
|
|
13
|
+
import { buildAndHashPoolScript, POOL_SHARE_PORT_BASE } from './pool-script.js';
|
|
14
|
+
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock, STATE(300+k)=share k. */
|
|
15
|
+
export function poolStateVariables(root, epoch, exitBlock, shares = []) {
|
|
16
16
|
return [
|
|
17
17
|
{ port: 100, value: root, type: 'string' },
|
|
18
18
|
{ port: 101, value: BigInt(epoch), type: 'number' },
|
|
19
19
|
{ port: 102, value: BigInt(exitBlock), type: 'number' },
|
|
20
|
+
...shares.map((share, k) => ({ port: POOL_SHARE_PORT_BASE + k, value: share, type: 'number' })),
|
|
20
21
|
];
|
|
21
22
|
}
|
|
22
23
|
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
23
24
|
export function buildBatchAnchorDraft(params) {
|
|
25
|
+
if (params.holders.length !== params.shares.length) {
|
|
26
|
+
throw new Error('anchorBatch: holders and shares must be the same length');
|
|
27
|
+
}
|
|
24
28
|
const { script, address } = buildAndHashPoolScript({
|
|
25
29
|
operatorPublicKeyDigest: params.operatorPublicKeyDigest,
|
|
26
|
-
|
|
30
|
+
holders: params.holders,
|
|
27
31
|
exitBlock: params.exitBlock,
|
|
28
32
|
});
|
|
29
33
|
// The input must be the operator's own pool output, not a foreign script.
|
|
@@ -33,7 +37,7 @@ export function buildBatchAnchorDraft(params) {
|
|
|
33
37
|
if (params.fundingAddress !== address) {
|
|
34
38
|
throw new Error('anchorBatch: fundingAddress does not match the pool address');
|
|
35
39
|
}
|
|
36
|
-
const stateVariables = poolStateVariables(params.root, params.epoch, params.exitBlock);
|
|
40
|
+
const stateVariables = poolStateVariables(params.root, params.epoch, params.exitBlock, params.shares);
|
|
37
41
|
return {
|
|
38
42
|
type: 'settlement',
|
|
39
43
|
inputs: [{
|
package/dist/cjs/anchor.d.ts
CHANGED
|
@@ -11,8 +11,11 @@ import { type OmniaTxDraft, type StateValue } from '@totemsdk/omnia';
|
|
|
11
11
|
import type { ChainStateProvider } from '@totemsdk/chain-provider';
|
|
12
12
|
export interface BatchAnchorParams {
|
|
13
13
|
operatorPublicKeyDigest: string;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/** Holder WOTS public-key digests (one per pool share). */
|
|
15
|
+
holders: string[];
|
|
16
|
+
/** Committed share per holder (aligned with `holders`); Σ = pool capacity. */
|
|
17
|
+
shares: bigint[];
|
|
18
|
+
/** Absolute block height after which holders may exit unilaterally (STATE(102)). */
|
|
16
19
|
exitBlock: number;
|
|
17
20
|
/** The epoch's Merkle root (32-byte hex). */
|
|
18
21
|
root: string;
|
|
@@ -25,8 +28,8 @@ export interface BatchAnchorParams {
|
|
|
25
28
|
amount: bigint;
|
|
26
29
|
tokenId: string;
|
|
27
30
|
}
|
|
28
|
-
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
|
|
29
|
-
export declare function poolStateVariables(root: string, epoch: number, exitBlock: number): StateValue[];
|
|
31
|
+
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock, STATE(300+k)=share k. */
|
|
32
|
+
export declare function poolStateVariables(root: string, epoch: number, exitBlock: number, shares?: bigint[]): StateValue[];
|
|
30
33
|
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
31
34
|
export declare function buildBatchAnchorDraft(params: BatchAnchorParams): OmniaTxDraft;
|
|
32
35
|
export declare function batchAnchorDigest(draft: OmniaTxDraft): Uint8Array;
|
package/dist/cjs/anchor.js
CHANGED
|
@@ -18,19 +18,23 @@ const omnia_1 = require("@totemsdk/omnia");
|
|
|
18
18
|
const txpow_1 = require("@totemsdk/txpow");
|
|
19
19
|
const core_1 = require("@totemsdk/core");
|
|
20
20
|
const pool_script_js_1 = require("./pool-script.js");
|
|
21
|
-
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock. */
|
|
22
|
-
function poolStateVariables(root, epoch, exitBlock) {
|
|
21
|
+
/** Pool output state: STATE(100)=root, STATE(101)=epoch, STATE(102)=exitBlock, STATE(300+k)=share k. */
|
|
22
|
+
function poolStateVariables(root, epoch, exitBlock, shares = []) {
|
|
23
23
|
return [
|
|
24
24
|
{ port: 100, value: root, type: 'string' },
|
|
25
25
|
{ port: 101, value: BigInt(epoch), type: 'number' },
|
|
26
26
|
{ port: 102, value: BigInt(exitBlock), type: 'number' },
|
|
27
|
+
...shares.map((share, k) => ({ port: pool_script_js_1.POOL_SHARE_PORT_BASE + k, value: share, type: 'number' })),
|
|
27
28
|
];
|
|
28
29
|
}
|
|
29
30
|
/** Build the batch-anchor draft (input = pool coin, output = pool state output). */
|
|
30
31
|
function buildBatchAnchorDraft(params) {
|
|
32
|
+
if (params.holders.length !== params.shares.length) {
|
|
33
|
+
throw new Error('anchorBatch: holders and shares must be the same length');
|
|
34
|
+
}
|
|
31
35
|
const { script, address } = (0, pool_script_js_1.buildAndHashPoolScript)({
|
|
32
36
|
operatorPublicKeyDigest: params.operatorPublicKeyDigest,
|
|
33
|
-
|
|
37
|
+
holders: params.holders,
|
|
34
38
|
exitBlock: params.exitBlock,
|
|
35
39
|
});
|
|
36
40
|
// The input must be the operator's own pool output, not a foreign script.
|
|
@@ -40,7 +44,7 @@ function buildBatchAnchorDraft(params) {
|
|
|
40
44
|
if (params.fundingAddress !== address) {
|
|
41
45
|
throw new Error('anchorBatch: fundingAddress does not match the pool address');
|
|
42
46
|
}
|
|
43
|
-
const stateVariables = poolStateVariables(params.root, params.epoch, params.exitBlock);
|
|
47
|
+
const stateVariables = poolStateVariables(params.root, params.epoch, params.exitBlock, params.shares);
|
|
44
48
|
return {
|
|
45
49
|
type: 'settlement',
|
|
46
50
|
inputs: [{
|
package/dist/cjs/exit-tx.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VTXO exit transaction (RFC-038
|
|
2
|
+
* VTXO exit transaction (RFC-038 V3 — per-share, owner-signed).
|
|
3
3
|
*
|
|
4
|
-
* The exit spends the pool's on-chain coin and pays the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (injected miner).
|
|
4
|
+
* The exit spends the pool's on-chain coin and pays the exiting holder **only
|
|
5
|
+
* its committed share** (`shares[holderIndex]`), returning the remainder to the
|
|
6
|
+
* pool with that holder's share state zeroed. It is signed with the holder's key
|
|
7
|
+
* — the pool script's `IF @BLOCK GT EXITBLOCK … IF SIGNEDBY(holder) … VERIFYOUT(0
|
|
8
|
+
* holder SHARE @TOKENID FALSE)` branch authorizes it, so the holder can broadcast
|
|
9
|
+
* without any operator signature or liveness, and cannot take more than its share.
|
|
11
10
|
*
|
|
12
|
-
* The caller must pass the
|
|
13
|
-
*
|
|
11
|
+
* The caller must pass the holder signer; neither the operator nor another holder
|
|
12
|
+
* can build a valid witness for this holder's share.
|
|
14
13
|
*/
|
|
15
14
|
import { type OmniaTxDraft } from '@totemsdk/omnia';
|
|
16
15
|
import type { ChainStateProvider } from '@totemsdk/chain-provider';
|
|
17
16
|
import type { OmniaVtxo } from './types.js';
|
|
18
|
-
/** State port carrying
|
|
17
|
+
/** State port carrying an optional exit delay on the holder's output. */
|
|
19
18
|
export declare const EXIT_DELAY_PORT = 110;
|
|
20
19
|
export interface ExitTxParams {
|
|
21
20
|
vtxo: OmniaVtxo;
|
|
@@ -23,12 +22,19 @@ export interface ExitTxParams {
|
|
|
23
22
|
fundingCoinId: string;
|
|
24
23
|
fundingAddress: string;
|
|
25
24
|
fundingScript: string;
|
|
26
|
-
/**
|
|
25
|
+
/** Total pool amount (raw base units). */
|
|
27
26
|
amount: bigint;
|
|
28
|
-
/**
|
|
29
|
-
|
|
27
|
+
/** Pool holders + committed shares (aligned arrays). */
|
|
28
|
+
holders: string[];
|
|
29
|
+
shares: bigint[];
|
|
30
|
+
/** Index of the exiting holder (must match `vtxo.owner`). */
|
|
31
|
+
holderIndex: number;
|
|
32
|
+
/** Pool state, needed to rebuild the remainder output's state. */
|
|
33
|
+
root: string;
|
|
34
|
+
epoch: number;
|
|
35
|
+
exitBlock: number;
|
|
30
36
|
}
|
|
31
|
-
/** Draft that spends the pool coin
|
|
37
|
+
/** Draft that spends the pool coin: output 0 = holder's share, remainder = pool. */
|
|
32
38
|
export declare function buildExitTransactionDraft(params: ExitTxParams): OmniaTxDraft;
|
|
33
39
|
export declare function exitTransactionDigest(draft: OmniaTxDraft): Uint8Array;
|
|
34
40
|
export declare function exitTransactionHex(draft: OmniaTxDraft): string;
|
|
@@ -44,14 +50,13 @@ export interface ExitTxDeps {
|
|
|
44
50
|
export interface ExitTxResult {
|
|
45
51
|
vtxoId: string;
|
|
46
52
|
owner: string;
|
|
47
|
-
|
|
48
|
-
exitDelayBlocks: number;
|
|
53
|
+
share: string;
|
|
49
54
|
draftType: 'vtxo-exit';
|
|
50
55
|
txHex: string;
|
|
51
56
|
digest: string;
|
|
52
57
|
witnessHex: string;
|
|
53
58
|
txpowId?: string;
|
|
54
59
|
}
|
|
55
|
-
/** Build, sign (and optionally mine + broadcast) a VTXO exit transaction. */
|
|
60
|
+
/** Build, sign (and optionally mine + broadcast) a per-share VTXO exit transaction. */
|
|
56
61
|
export declare function buildExitTransaction(params: ExitTxParams, deps: ExitTxDeps): Promise<ExitTxResult>;
|
|
57
62
|
export {};
|
package/dist/cjs/exit-tx.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* VTXO exit transaction (RFC-038
|
|
3
|
+
* VTXO exit transaction (RFC-038 V3 — per-share, owner-signed).
|
|
4
4
|
*
|
|
5
|
-
* The exit spends the pool's on-chain coin and pays the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (injected miner).
|
|
5
|
+
* The exit spends the pool's on-chain coin and pays the exiting holder **only
|
|
6
|
+
* its committed share** (`shares[holderIndex]`), returning the remainder to the
|
|
7
|
+
* pool with that holder's share state zeroed. It is signed with the holder's key
|
|
8
|
+
* — the pool script's `IF @BLOCK GT EXITBLOCK … IF SIGNEDBY(holder) … VERIFYOUT(0
|
|
9
|
+
* holder SHARE @TOKENID FALSE)` branch authorizes it, so the holder can broadcast
|
|
10
|
+
* without any operator signature or liveness, and cannot take more than its share.
|
|
12
11
|
*
|
|
13
|
-
* The caller must pass the
|
|
14
|
-
*
|
|
12
|
+
* The caller must pass the holder signer; neither the operator nor another holder
|
|
13
|
+
* can build a valid witness for this holder's share.
|
|
15
14
|
*/
|
|
16
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
16
|
exports.EXIT_DELAY_PORT = void 0;
|
|
@@ -22,13 +21,34 @@ exports.buildExitTransaction = buildExitTransaction;
|
|
|
22
21
|
const omnia_1 = require("@totemsdk/omnia");
|
|
23
22
|
const txpow_1 = require("@totemsdk/txpow");
|
|
24
23
|
const core_1 = require("@totemsdk/core");
|
|
25
|
-
|
|
24
|
+
const anchor_js_1 = require("./anchor.js");
|
|
25
|
+
/** State port carrying an optional exit delay on the holder's output. */
|
|
26
26
|
exports.EXIT_DELAY_PORT = 110;
|
|
27
|
-
/** Draft that spends the pool coin
|
|
27
|
+
/** Draft that spends the pool coin: output 0 = holder's share, remainder = pool. */
|
|
28
28
|
function buildExitTransactionDraft(params) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const share = params.shares[params.holderIndex];
|
|
30
|
+
if (share === undefined)
|
|
31
|
+
throw new Error('exit: holderIndex out of range');
|
|
32
|
+
if (share <= 0n)
|
|
33
|
+
throw new Error('exit: holder share must be positive');
|
|
34
|
+
const outputs = [{
|
|
35
|
+
address: params.vtxo.owner,
|
|
36
|
+
amount: share,
|
|
37
|
+
tokenId: params.vtxo.tokenId,
|
|
38
|
+
storeState: false,
|
|
39
|
+
stateVariables: [],
|
|
40
|
+
}];
|
|
41
|
+
const carriesState = params.amount > share;
|
|
42
|
+
if (carriesState) {
|
|
43
|
+
const remaining = params.shares.map((s, i) => (i === params.holderIndex ? 0n : s));
|
|
44
|
+
outputs.push({
|
|
45
|
+
address: params.fundingAddress,
|
|
46
|
+
amount: params.amount - share,
|
|
47
|
+
tokenId: params.vtxo.tokenId,
|
|
48
|
+
storeState: true,
|
|
49
|
+
stateVariables: (0, anchor_js_1.poolStateVariables)(params.root, params.epoch, params.exitBlock, remaining),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
32
52
|
return {
|
|
33
53
|
type: 'settlement',
|
|
34
54
|
inputs: [{
|
|
@@ -38,15 +58,9 @@ function buildExitTransactionDraft(params) {
|
|
|
38
58
|
tokenId: params.vtxo.tokenId,
|
|
39
59
|
scriptHex: params.fundingScript,
|
|
40
60
|
}],
|
|
41
|
-
outputs
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
tokenId: params.vtxo.tokenId,
|
|
45
|
-
storeState: false,
|
|
46
|
-
stateVariables,
|
|
47
|
-
}],
|
|
48
|
-
storeState: false,
|
|
49
|
-
stateVariables,
|
|
61
|
+
outputs,
|
|
62
|
+
storeState: carriesState,
|
|
63
|
+
stateVariables: [],
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
function exitTransactionDigest(draft) {
|
|
@@ -55,7 +69,7 @@ function exitTransactionDigest(draft) {
|
|
|
55
69
|
function exitTransactionHex(draft) {
|
|
56
70
|
return (0, omnia_1.serializeTxDraft)(draft);
|
|
57
71
|
}
|
|
58
|
-
/** Build, sign (and optionally mine + broadcast) a VTXO exit transaction. */
|
|
72
|
+
/** Build, sign (and optionally mine + broadcast) a per-share VTXO exit transaction. */
|
|
59
73
|
async function buildExitTransaction(params, deps) {
|
|
60
74
|
const draft = buildExitTransactionDraft(params);
|
|
61
75
|
const digest = exitTransactionDigest(draft);
|
|
@@ -63,8 +77,7 @@ async function buildExitTransaction(params, deps) {
|
|
|
63
77
|
const result = {
|
|
64
78
|
vtxoId: params.vtxo.vtxoId,
|
|
65
79
|
owner: params.vtxo.owner,
|
|
66
|
-
|
|
67
|
-
exitDelayBlocks: params.exitDelayBlocks,
|
|
80
|
+
share: params.shares[params.holderIndex].toString(),
|
|
68
81
|
draftType: 'vtxo-exit',
|
|
69
82
|
txHex: exitTransactionHex(draft),
|
|
70
83
|
digest: Buffer.from(digest).toString('hex'),
|
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VTXO pool on-chain script (RFC-038
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V3 — per-share non-custodial).
|
|
3
3
|
*
|
|
4
4
|
* The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
|
|
5
|
-
* root is committed as STATE(100), the epoch number in STATE(101),
|
|
6
|
-
*
|
|
5
|
+
* root is committed as STATE(100), the epoch number in STATE(101), the absolute
|
|
6
|
+
* exit-eligibility block in STATE(102), and each holder's committed share in
|
|
7
|
+
* STATE(200+k).
|
|
7
8
|
*
|
|
8
|
-
* Custody (RFC-
|
|
9
|
-
* - **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Custody (RFC-039 / CUSTODY-POLICY §2):
|
|
10
|
+
* - **Per-share unilateral exit.** After `@BLOCK GT PREVSTATE(102)` each holder
|
|
11
|
+
* may withdraw **only its own committed share** (`PREVSTATE(200+k)`, the spent
|
|
12
|
+
* coin's share), to its own key, zeroing that share in the successor
|
|
13
|
+
* (`STATE(200+k) EQ 0`) and returning the remainder to the pool. A holder
|
|
14
|
+
* cannot pay itself more than its share, so one holder can never rug the others.
|
|
12
15
|
* - **Cooperative update.** Before the exit window the pool may only move by
|
|
13
|
-
* `MULTISIG(
|
|
16
|
+
* `MULTISIG(operator + all holders)`, epoch-monotone (`ASSERT EPOCH GT
|
|
14
17
|
* PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
|
|
15
|
-
* 102)`) so
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* 102)`) so no one can push the escape clock, and value confined to the same
|
|
19
|
+
* script address (`ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)`)
|
|
20
|
+
* so the operator can never pay itself.
|
|
18
21
|
*
|
|
19
|
-
* The operator holds no unilateral path: it cannot move funds, cannot advance
|
|
20
|
-
*
|
|
22
|
+
* The operator holds no unilateral path: it cannot move funds, cannot advance the
|
|
23
|
+
* exit block, and cannot stop a holder's exit. It is a liveness-only peer.
|
|
21
24
|
*/
|
|
25
|
+
/** State port base for per-holder shares: holder k ⇒ port 200+k (ports are bytes, ≤255). */
|
|
26
|
+
export declare const POOL_SHARE_PORT_BASE = 200;
|
|
22
27
|
export interface PoolScriptParams {
|
|
23
28
|
/** Operator WOTS public-key digest (64 hex). */
|
|
24
29
|
operatorPublicKeyDigest: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/** Absolute block height after which
|
|
30
|
+
/** Holder WOTS public-key digests (64 hex), one per beneficial owner. */
|
|
31
|
+
holders: string[];
|
|
32
|
+
/** Absolute block height after which holders may exit unilaterally. */
|
|
28
33
|
exitBlock: number;
|
|
29
34
|
}
|
|
30
|
-
/**
|
|
35
|
+
/** Per-share-constrained, epoch-monotone, state-carrying pool script. */
|
|
31
36
|
export declare function buildPoolScript(params: PoolScriptParams): string;
|
|
32
37
|
export declare function poolScriptAddress(script: string): string;
|
|
33
38
|
export declare function buildAndHashPoolScript(params: PoolScriptParams): {
|
package/dist/cjs/pool-script.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* VTXO pool on-chain script (RFC-038
|
|
3
|
+
* VTXO pool on-chain script (RFC-038 V3 — per-share non-custodial).
|
|
4
4
|
*
|
|
5
5
|
* The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
|
|
6
|
-
* root is committed as STATE(100), the epoch number in STATE(101),
|
|
7
|
-
*
|
|
6
|
+
* root is committed as STATE(100), the epoch number in STATE(101), the absolute
|
|
7
|
+
* exit-eligibility block in STATE(102), and each holder's committed share in
|
|
8
|
+
* STATE(200+k).
|
|
8
9
|
*
|
|
9
|
-
* Custody (RFC-
|
|
10
|
-
* - **
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Custody (RFC-039 / CUSTODY-POLICY §2):
|
|
11
|
+
* - **Per-share unilateral exit.** After `@BLOCK GT PREVSTATE(102)` each holder
|
|
12
|
+
* may withdraw **only its own committed share** (`PREVSTATE(200+k)`, the spent
|
|
13
|
+
* coin's share), to its own key, zeroing that share in the successor
|
|
14
|
+
* (`STATE(200+k) EQ 0`) and returning the remainder to the pool. A holder
|
|
15
|
+
* cannot pay itself more than its share, so one holder can never rug the others.
|
|
13
16
|
* - **Cooperative update.** Before the exit window the pool may only move by
|
|
14
|
-
* `MULTISIG(
|
|
17
|
+
* `MULTISIG(operator + all holders)`, epoch-monotone (`ASSERT EPOCH GT
|
|
15
18
|
* PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
|
|
16
|
-
* 102)`) so
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* 102)`) so no one can push the escape clock, and value confined to the same
|
|
20
|
+
* script address (`ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)`)
|
|
21
|
+
* so the operator can never pay itself.
|
|
19
22
|
*
|
|
20
|
-
* The operator holds no unilateral path: it cannot move funds, cannot advance
|
|
21
|
-
*
|
|
23
|
+
* The operator holds no unilateral path: it cannot move funds, cannot advance the
|
|
24
|
+
* exit block, and cannot stop a holder's exit. It is a liveness-only peer.
|
|
22
25
|
*/
|
|
23
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.POOL_SHARE_PORT_BASE = void 0;
|
|
24
28
|
exports.buildPoolScript = buildPoolScript;
|
|
25
29
|
exports.poolScriptAddress = poolScriptAddress;
|
|
26
30
|
exports.buildAndHashPoolScript = buildAndHashPoolScript;
|
|
27
31
|
const core_1 = require("@totemsdk/core");
|
|
32
|
+
/** State port base for per-holder shares: holder k ⇒ port 200+k (ports are bytes, ≤255). */
|
|
33
|
+
exports.POOL_SHARE_PORT_BASE = 200;
|
|
28
34
|
function pkHex(digest, label) {
|
|
29
35
|
const raw = digest.replace(/^0x/i, '');
|
|
30
36
|
if (!/^[0-9a-fA-F]{64}$/.test(raw)) {
|
|
@@ -32,24 +38,48 @@ function pkHex(digest, label) {
|
|
|
32
38
|
}
|
|
33
39
|
return '0X' + raw.toUpperCase();
|
|
34
40
|
}
|
|
35
|
-
/**
|
|
41
|
+
/** Per-share-constrained, epoch-monotone, state-carrying pool script. */
|
|
36
42
|
function buildPoolScript(params) {
|
|
37
|
-
const owner = pkHex(params.ownerPublicKeyDigest, 'owner');
|
|
38
43
|
const operator = pkHex(params.operatorPublicKeyDigest, 'operator');
|
|
44
|
+
if (!Array.isArray(params.holders) || params.holders.length < 1) {
|
|
45
|
+
throw new Error('pool requires at least one holder');
|
|
46
|
+
}
|
|
47
|
+
const holders = params.holders.map((h, i) => pkHex(h, `holder[${i}]`));
|
|
39
48
|
if (!Number.isInteger(params.exitBlock) || params.exitBlock < 0) {
|
|
40
49
|
throw new Error(`exitBlock must be a non-negative integer, got ${JSON.stringify(params.exitBlock)}`);
|
|
41
50
|
}
|
|
42
|
-
|
|
51
|
+
const lines = [
|
|
43
52
|
'LET EPOCH=STATE(101)',
|
|
44
|
-
'LET EXITBLOCK=
|
|
53
|
+
'LET EXITBLOCK=PREVSTATE(102)',
|
|
45
54
|
'IF @BLOCK GT EXITBLOCK THEN',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
|
|
55
|
+
];
|
|
56
|
+
holders.forEach((holder, k) => {
|
|
57
|
+
const port = exports.POOL_SHARE_PORT_BASE + k;
|
|
58
|
+
lines.push(` LET SHARE${k}=PREVSTATE(${port})`);
|
|
59
|
+
lines.push(` IF SIGNEDBY(${holder}) THEN`);
|
|
60
|
+
lines.push(` IF SHARE${k} GT 0 THEN`);
|
|
61
|
+
lines.push(` ASSERT VERIFYOUT(0 ${holder} SHARE${k} @TOKENID FALSE)`);
|
|
62
|
+
for (let j = 0; j < holders.length; j++) {
|
|
63
|
+
if (j === k)
|
|
64
|
+
continue;
|
|
65
|
+
lines.push(` ASSERT STATE(${exports.POOL_SHARE_PORT_BASE + j}) EQ PREVSTATE(${exports.POOL_SHARE_PORT_BASE + j})`);
|
|
66
|
+
}
|
|
67
|
+
lines.push(` ASSERT STATE(${port}) EQ 0`);
|
|
68
|
+
// Remainder (other holders' shares) stays in the pool; skip it when the
|
|
69
|
+
// withdrawing holder's share is the whole pool (no room for another output).
|
|
70
|
+
lines.push(` IF @AMOUNT GT SHARE${k} THEN`);
|
|
71
|
+
lines.push(` RETURN VERIFYOUT(@INPUT @ADDRESS @AMOUNT SUB SHARE${k} @TOKENID TRUE)`);
|
|
72
|
+
lines.push(' ENDIF');
|
|
73
|
+
lines.push(' RETURN TRUE');
|
|
74
|
+
lines.push(' ENDIF');
|
|
75
|
+
lines.push(' ENDIF');
|
|
76
|
+
});
|
|
77
|
+
lines.push('ENDIF');
|
|
78
|
+
lines.push('ASSERT EPOCH GT PREVSTATE(101)');
|
|
79
|
+
lines.push('ASSERT SAMESTATE(102 102)');
|
|
80
|
+
lines.push('ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)');
|
|
81
|
+
lines.push(`RETURN MULTISIG(${holders.length + 1} ${operator} ${holders.join(' ')})`);
|
|
82
|
+
return lines.join('\n');
|
|
53
83
|
}
|
|
54
84
|
function poolScriptAddress(script) {
|
|
55
85
|
return (0, core_1.computeScriptAddress)(script);
|
package/dist/exit-tx.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VTXO exit transaction (RFC-038
|
|
2
|
+
* VTXO exit transaction (RFC-038 V3 — per-share, owner-signed).
|
|
3
3
|
*
|
|
4
|
-
* The exit spends the pool's on-chain coin and pays the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (injected miner).
|
|
4
|
+
* The exit spends the pool's on-chain coin and pays the exiting holder **only
|
|
5
|
+
* its committed share** (`shares[holderIndex]`), returning the remainder to the
|
|
6
|
+
* pool with that holder's share state zeroed. It is signed with the holder's key
|
|
7
|
+
* — the pool script's `IF @BLOCK GT EXITBLOCK … IF SIGNEDBY(holder) … VERIFYOUT(0
|
|
8
|
+
* holder SHARE @TOKENID FALSE)` branch authorizes it, so the holder can broadcast
|
|
9
|
+
* without any operator signature or liveness, and cannot take more than its share.
|
|
11
10
|
*
|
|
12
|
-
* The caller must pass the
|
|
13
|
-
*
|
|
11
|
+
* The caller must pass the holder signer; neither the operator nor another holder
|
|
12
|
+
* can build a valid witness for this holder's share.
|
|
14
13
|
*/
|
|
15
14
|
import { type OmniaTxDraft } from '@totemsdk/omnia';
|
|
16
15
|
import type { ChainStateProvider } from '@totemsdk/chain-provider';
|
|
17
16
|
import type { OmniaVtxo } from './types.js';
|
|
18
|
-
/** State port carrying
|
|
17
|
+
/** State port carrying an optional exit delay on the holder's output. */
|
|
19
18
|
export declare const EXIT_DELAY_PORT = 110;
|
|
20
19
|
export interface ExitTxParams {
|
|
21
20
|
vtxo: OmniaVtxo;
|
|
@@ -23,12 +22,19 @@ export interface ExitTxParams {
|
|
|
23
22
|
fundingCoinId: string;
|
|
24
23
|
fundingAddress: string;
|
|
25
24
|
fundingScript: string;
|
|
26
|
-
/**
|
|
25
|
+
/** Total pool amount (raw base units). */
|
|
27
26
|
amount: bigint;
|
|
28
|
-
/**
|
|
29
|
-
|
|
27
|
+
/** Pool holders + committed shares (aligned arrays). */
|
|
28
|
+
holders: string[];
|
|
29
|
+
shares: bigint[];
|
|
30
|
+
/** Index of the exiting holder (must match `vtxo.owner`). */
|
|
31
|
+
holderIndex: number;
|
|
32
|
+
/** Pool state, needed to rebuild the remainder output's state. */
|
|
33
|
+
root: string;
|
|
34
|
+
epoch: number;
|
|
35
|
+
exitBlock: number;
|
|
30
36
|
}
|
|
31
|
-
/** Draft that spends the pool coin
|
|
37
|
+
/** Draft that spends the pool coin: output 0 = holder's share, remainder = pool. */
|
|
32
38
|
export declare function buildExitTransactionDraft(params: ExitTxParams): OmniaTxDraft;
|
|
33
39
|
export declare function exitTransactionDigest(draft: OmniaTxDraft): Uint8Array;
|
|
34
40
|
export declare function exitTransactionHex(draft: OmniaTxDraft): string;
|
|
@@ -44,14 +50,13 @@ export interface ExitTxDeps {
|
|
|
44
50
|
export interface ExitTxResult {
|
|
45
51
|
vtxoId: string;
|
|
46
52
|
owner: string;
|
|
47
|
-
|
|
48
|
-
exitDelayBlocks: number;
|
|
53
|
+
share: string;
|
|
49
54
|
draftType: 'vtxo-exit';
|
|
50
55
|
txHex: string;
|
|
51
56
|
digest: string;
|
|
52
57
|
witnessHex: string;
|
|
53
58
|
txpowId?: string;
|
|
54
59
|
}
|
|
55
|
-
/** Build, sign (and optionally mine + broadcast) a VTXO exit transaction. */
|
|
60
|
+
/** Build, sign (and optionally mine + broadcast) a per-share VTXO exit transaction. */
|
|
56
61
|
export declare function buildExitTransaction(params: ExitTxParams, deps: ExitTxDeps): Promise<ExitTxResult>;
|
|
57
62
|
export {};
|
package/dist/exit-tx.js
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VTXO exit transaction (RFC-038
|
|
2
|
+
* VTXO exit transaction (RFC-038 V3 — per-share, owner-signed).
|
|
3
3
|
*
|
|
4
|
-
* The exit spends the pool's on-chain coin and pays the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (injected miner).
|
|
4
|
+
* The exit spends the pool's on-chain coin and pays the exiting holder **only
|
|
5
|
+
* its committed share** (`shares[holderIndex]`), returning the remainder to the
|
|
6
|
+
* pool with that holder's share state zeroed. It is signed with the holder's key
|
|
7
|
+
* — the pool script's `IF @BLOCK GT EXITBLOCK … IF SIGNEDBY(holder) … VERIFYOUT(0
|
|
8
|
+
* holder SHARE @TOKENID FALSE)` branch authorizes it, so the holder can broadcast
|
|
9
|
+
* without any operator signature or liveness, and cannot take more than its share.
|
|
11
10
|
*
|
|
12
|
-
* The caller must pass the
|
|
13
|
-
*
|
|
11
|
+
* The caller must pass the holder signer; neither the operator nor another holder
|
|
12
|
+
* can build a valid witness for this holder's share.
|
|
14
13
|
*/
|
|
15
14
|
import { computeOmniaTxDigest, omniaDraftToMinimaBytes, serializeTxDraft, } from '@totemsdk/omnia';
|
|
16
15
|
import { serializeTxBody, mineTxPoW, TX_POW_MIN_DIFFICULTY } from '@totemsdk/txpow';
|
|
17
16
|
import { concatBytes } from '@totemsdk/core';
|
|
18
|
-
|
|
17
|
+
import { poolStateVariables } from './anchor.js';
|
|
18
|
+
/** State port carrying an optional exit delay on the holder's output. */
|
|
19
19
|
export const EXIT_DELAY_PORT = 110;
|
|
20
|
-
/** Draft that spends the pool coin
|
|
20
|
+
/** Draft that spends the pool coin: output 0 = holder's share, remainder = pool. */
|
|
21
21
|
export function buildExitTransactionDraft(params) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const share = params.shares[params.holderIndex];
|
|
23
|
+
if (share === undefined)
|
|
24
|
+
throw new Error('exit: holderIndex out of range');
|
|
25
|
+
if (share <= 0n)
|
|
26
|
+
throw new Error('exit: holder share must be positive');
|
|
27
|
+
const outputs = [{
|
|
28
|
+
address: params.vtxo.owner,
|
|
29
|
+
amount: share,
|
|
30
|
+
tokenId: params.vtxo.tokenId,
|
|
31
|
+
storeState: false,
|
|
32
|
+
stateVariables: [],
|
|
33
|
+
}];
|
|
34
|
+
const carriesState = params.amount > share;
|
|
35
|
+
if (carriesState) {
|
|
36
|
+
const remaining = params.shares.map((s, i) => (i === params.holderIndex ? 0n : s));
|
|
37
|
+
outputs.push({
|
|
38
|
+
address: params.fundingAddress,
|
|
39
|
+
amount: params.amount - share,
|
|
40
|
+
tokenId: params.vtxo.tokenId,
|
|
41
|
+
storeState: true,
|
|
42
|
+
stateVariables: poolStateVariables(params.root, params.epoch, params.exitBlock, remaining),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
25
45
|
return {
|
|
26
46
|
type: 'settlement',
|
|
27
47
|
inputs: [{
|
|
@@ -31,15 +51,9 @@ export function buildExitTransactionDraft(params) {
|
|
|
31
51
|
tokenId: params.vtxo.tokenId,
|
|
32
52
|
scriptHex: params.fundingScript,
|
|
33
53
|
}],
|
|
34
|
-
outputs
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
tokenId: params.vtxo.tokenId,
|
|
38
|
-
storeState: false,
|
|
39
|
-
stateVariables,
|
|
40
|
-
}],
|
|
41
|
-
storeState: false,
|
|
42
|
-
stateVariables,
|
|
54
|
+
outputs,
|
|
55
|
+
storeState: carriesState,
|
|
56
|
+
stateVariables: [],
|
|
43
57
|
};
|
|
44
58
|
}
|
|
45
59
|
export function exitTransactionDigest(draft) {
|
|
@@ -48,7 +62,7 @@ export function exitTransactionDigest(draft) {
|
|
|
48
62
|
export function exitTransactionHex(draft) {
|
|
49
63
|
return serializeTxDraft(draft);
|
|
50
64
|
}
|
|
51
|
-
/** Build, sign (and optionally mine + broadcast) a VTXO exit transaction. */
|
|
65
|
+
/** Build, sign (and optionally mine + broadcast) a per-share VTXO exit transaction. */
|
|
52
66
|
export async function buildExitTransaction(params, deps) {
|
|
53
67
|
const draft = buildExitTransactionDraft(params);
|
|
54
68
|
const digest = exitTransactionDigest(draft);
|
|
@@ -56,8 +70,7 @@ export async function buildExitTransaction(params, deps) {
|
|
|
56
70
|
const result = {
|
|
57
71
|
vtxoId: params.vtxo.vtxoId,
|
|
58
72
|
owner: params.vtxo.owner,
|
|
59
|
-
|
|
60
|
-
exitDelayBlocks: params.exitDelayBlocks,
|
|
73
|
+
share: params.shares[params.holderIndex].toString(),
|
|
61
74
|
draftType: 'vtxo-exit',
|
|
62
75
|
txHex: exitTransactionHex(draft),
|
|
63
76
|
digest: Buffer.from(digest).toString('hex'),
|
package/dist/pool-script.d.ts
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VTXO pool on-chain script (RFC-038
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V3 — per-share non-custodial).
|
|
3
3
|
*
|
|
4
4
|
* The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
|
|
5
|
-
* root is committed as STATE(100), the epoch number in STATE(101),
|
|
6
|
-
*
|
|
5
|
+
* root is committed as STATE(100), the epoch number in STATE(101), the absolute
|
|
6
|
+
* exit-eligibility block in STATE(102), and each holder's committed share in
|
|
7
|
+
* STATE(200+k).
|
|
7
8
|
*
|
|
8
|
-
* Custody (RFC-
|
|
9
|
-
* - **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Custody (RFC-039 / CUSTODY-POLICY §2):
|
|
10
|
+
* - **Per-share unilateral exit.** After `@BLOCK GT PREVSTATE(102)` each holder
|
|
11
|
+
* may withdraw **only its own committed share** (`PREVSTATE(200+k)`, the spent
|
|
12
|
+
* coin's share), to its own key, zeroing that share in the successor
|
|
13
|
+
* (`STATE(200+k) EQ 0`) and returning the remainder to the pool. A holder
|
|
14
|
+
* cannot pay itself more than its share, so one holder can never rug the others.
|
|
12
15
|
* - **Cooperative update.** Before the exit window the pool may only move by
|
|
13
|
-
* `MULTISIG(
|
|
16
|
+
* `MULTISIG(operator + all holders)`, epoch-monotone (`ASSERT EPOCH GT
|
|
14
17
|
* PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
|
|
15
|
-
* 102)`) so
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* 102)`) so no one can push the escape clock, and value confined to the same
|
|
19
|
+
* script address (`ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)`)
|
|
20
|
+
* so the operator can never pay itself.
|
|
18
21
|
*
|
|
19
|
-
* The operator holds no unilateral path: it cannot move funds, cannot advance
|
|
20
|
-
*
|
|
22
|
+
* The operator holds no unilateral path: it cannot move funds, cannot advance the
|
|
23
|
+
* exit block, and cannot stop a holder's exit. It is a liveness-only peer.
|
|
21
24
|
*/
|
|
25
|
+
/** State port base for per-holder shares: holder k ⇒ port 200+k (ports are bytes, ≤255). */
|
|
26
|
+
export declare const POOL_SHARE_PORT_BASE = 200;
|
|
22
27
|
export interface PoolScriptParams {
|
|
23
28
|
/** Operator WOTS public-key digest (64 hex). */
|
|
24
29
|
operatorPublicKeyDigest: string;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/** Absolute block height after which
|
|
30
|
+
/** Holder WOTS public-key digests (64 hex), one per beneficial owner. */
|
|
31
|
+
holders: string[];
|
|
32
|
+
/** Absolute block height after which holders may exit unilaterally. */
|
|
28
33
|
exitBlock: number;
|
|
29
34
|
}
|
|
30
|
-
/**
|
|
35
|
+
/** Per-share-constrained, epoch-monotone, state-carrying pool script. */
|
|
31
36
|
export declare function buildPoolScript(params: PoolScriptParams): string;
|
|
32
37
|
export declare function poolScriptAddress(script: string): string;
|
|
33
38
|
export declare function buildAndHashPoolScript(params: PoolScriptParams): {
|
package/dist/pool-script.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* VTXO pool on-chain script (RFC-038
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V3 — per-share non-custodial).
|
|
3
3
|
*
|
|
4
4
|
* The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
|
|
5
|
-
* root is committed as STATE(100), the epoch number in STATE(101),
|
|
6
|
-
*
|
|
5
|
+
* root is committed as STATE(100), the epoch number in STATE(101), the absolute
|
|
6
|
+
* exit-eligibility block in STATE(102), and each holder's committed share in
|
|
7
|
+
* STATE(200+k).
|
|
7
8
|
*
|
|
8
|
-
* Custody (RFC-
|
|
9
|
-
* - **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Custody (RFC-039 / CUSTODY-POLICY §2):
|
|
10
|
+
* - **Per-share unilateral exit.** After `@BLOCK GT PREVSTATE(102)` each holder
|
|
11
|
+
* may withdraw **only its own committed share** (`PREVSTATE(200+k)`, the spent
|
|
12
|
+
* coin's share), to its own key, zeroing that share in the successor
|
|
13
|
+
* (`STATE(200+k) EQ 0`) and returning the remainder to the pool. A holder
|
|
14
|
+
* cannot pay itself more than its share, so one holder can never rug the others.
|
|
12
15
|
* - **Cooperative update.** Before the exit window the pool may only move by
|
|
13
|
-
* `MULTISIG(
|
|
16
|
+
* `MULTISIG(operator + all holders)`, epoch-monotone (`ASSERT EPOCH GT
|
|
14
17
|
* PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
|
|
15
|
-
* 102)`) so
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* 102)`) so no one can push the escape clock, and value confined to the same
|
|
19
|
+
* script address (`ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)`)
|
|
20
|
+
* so the operator can never pay itself.
|
|
18
21
|
*
|
|
19
|
-
* The operator holds no unilateral path: it cannot move funds, cannot advance
|
|
20
|
-
*
|
|
22
|
+
* The operator holds no unilateral path: it cannot move funds, cannot advance the
|
|
23
|
+
* exit block, and cannot stop a holder's exit. It is a liveness-only peer.
|
|
21
24
|
*/
|
|
22
25
|
import { computeScriptAddress } from '@totemsdk/core';
|
|
26
|
+
/** State port base for per-holder shares: holder k ⇒ port 200+k (ports are bytes, ≤255). */
|
|
27
|
+
export const POOL_SHARE_PORT_BASE = 200;
|
|
23
28
|
function pkHex(digest, label) {
|
|
24
29
|
const raw = digest.replace(/^0x/i, '');
|
|
25
30
|
if (!/^[0-9a-fA-F]{64}$/.test(raw)) {
|
|
@@ -27,24 +32,48 @@ function pkHex(digest, label) {
|
|
|
27
32
|
}
|
|
28
33
|
return '0X' + raw.toUpperCase();
|
|
29
34
|
}
|
|
30
|
-
/**
|
|
35
|
+
/** Per-share-constrained, epoch-monotone, state-carrying pool script. */
|
|
31
36
|
export function buildPoolScript(params) {
|
|
32
|
-
const owner = pkHex(params.ownerPublicKeyDigest, 'owner');
|
|
33
37
|
const operator = pkHex(params.operatorPublicKeyDigest, 'operator');
|
|
38
|
+
if (!Array.isArray(params.holders) || params.holders.length < 1) {
|
|
39
|
+
throw new Error('pool requires at least one holder');
|
|
40
|
+
}
|
|
41
|
+
const holders = params.holders.map((h, i) => pkHex(h, `holder[${i}]`));
|
|
34
42
|
if (!Number.isInteger(params.exitBlock) || params.exitBlock < 0) {
|
|
35
43
|
throw new Error(`exitBlock must be a non-negative integer, got ${JSON.stringify(params.exitBlock)}`);
|
|
36
44
|
}
|
|
37
|
-
|
|
45
|
+
const lines = [
|
|
38
46
|
'LET EPOCH=STATE(101)',
|
|
39
|
-
'LET EXITBLOCK=
|
|
47
|
+
'LET EXITBLOCK=PREVSTATE(102)',
|
|
40
48
|
'IF @BLOCK GT EXITBLOCK THEN',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
|
|
49
|
+
];
|
|
50
|
+
holders.forEach((holder, k) => {
|
|
51
|
+
const port = POOL_SHARE_PORT_BASE + k;
|
|
52
|
+
lines.push(` LET SHARE${k}=PREVSTATE(${port})`);
|
|
53
|
+
lines.push(` IF SIGNEDBY(${holder}) THEN`);
|
|
54
|
+
lines.push(` IF SHARE${k} GT 0 THEN`);
|
|
55
|
+
lines.push(` ASSERT VERIFYOUT(0 ${holder} SHARE${k} @TOKENID FALSE)`);
|
|
56
|
+
for (let j = 0; j < holders.length; j++) {
|
|
57
|
+
if (j === k)
|
|
58
|
+
continue;
|
|
59
|
+
lines.push(` ASSERT STATE(${POOL_SHARE_PORT_BASE + j}) EQ PREVSTATE(${POOL_SHARE_PORT_BASE + j})`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(` ASSERT STATE(${port}) EQ 0`);
|
|
62
|
+
// Remainder (other holders' shares) stays in the pool; skip it when the
|
|
63
|
+
// withdrawing holder's share is the whole pool (no room for another output).
|
|
64
|
+
lines.push(` IF @AMOUNT GT SHARE${k} THEN`);
|
|
65
|
+
lines.push(` RETURN VERIFYOUT(@INPUT @ADDRESS @AMOUNT SUB SHARE${k} @TOKENID TRUE)`);
|
|
66
|
+
lines.push(' ENDIF');
|
|
67
|
+
lines.push(' RETURN TRUE');
|
|
68
|
+
lines.push(' ENDIF');
|
|
69
|
+
lines.push(' ENDIF');
|
|
70
|
+
});
|
|
71
|
+
lines.push('ENDIF');
|
|
72
|
+
lines.push('ASSERT EPOCH GT PREVSTATE(101)');
|
|
73
|
+
lines.push('ASSERT SAMESTATE(102 102)');
|
|
74
|
+
lines.push('ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)');
|
|
75
|
+
lines.push(`RETURN MULTISIG(${holders.length + 1} ${operator} ${holders.join(' ')})`);
|
|
76
|
+
return lines.join('\n');
|
|
48
77
|
}
|
|
49
78
|
export function poolScriptAddress(script) {
|
|
50
79
|
return computeScriptAddress(script);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/omnia-vtxo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Virtual UTXO / payment-pool claim layer for Omnia — cash-like off-chain balance primitive for Totem Edge",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"types": "dist/cjs/index.d.ts",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@totemsdk/chain-provider": "^0.2.1",
|
|
28
|
-
"@totemsdk/
|
|
28
|
+
"@totemsdk/core": "^1.3.0",
|
|
29
|
+
"@totemsdk/omnia": "^2.0.1",
|
|
29
30
|
"@totemsdk/storage": "^0.1.0",
|
|
30
31
|
"@totemsdk/txpow": "^1.0.2",
|
|
31
|
-
"@totemsdk/core": "^1.2.11",
|
|
32
32
|
"@totemsdk/wots-lease": "^1.0.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|