@geeqdev/geeq-sdk 0.1.0 → 0.1.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/.idea/geeq_sdk.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.nvmrc +1 -0
- package/CONTRIBUTING.md +20 -0
- package/example/.env.example +4 -0
- package/example/README.md +304 -0
- package/example/USAGE.md +74 -0
- package/example/index.html +12 -0
- package/example/package-lock.json +1557 -0
- package/example/package.json +25 -0
- package/example/scripts/browser-check.mjs +1421 -0
- package/example/scripts/match-derivation.mjs +65 -0
- package/example/src/App.vue +138 -0
- package/example/src/components/AuthorityPanel.vue +277 -0
- package/example/src/components/ChainPanel.vue +267 -0
- package/example/src/components/ChainState.vue +59 -0
- package/example/src/components/CopyButton.vue +25 -0
- package/example/src/components/CreateAssetPanel.vue +313 -0
- package/example/src/components/DigitalAssetsPanel.vue +68 -0
- package/example/src/components/EvidencePanel.vue +396 -0
- package/example/src/components/ExplorerPanel.vue +119 -0
- package/example/src/components/KeypairPanel.vue +182 -0
- package/example/src/components/PhrasePanel.vue +105 -0
- package/example/src/components/SocketWarning.vue +18 -0
- package/example/src/components/StepGate.vue +24 -0
- package/example/src/components/TransferPanel.vue +416 -0
- package/example/src/components/TxDeadline.vue +19 -0
- package/example/src/components/TxFee.vue +31 -0
- package/example/src/components/TxStatus.vue +105 -0
- package/example/src/composables/useSocketConnection.ts +80 -0
- package/example/src/env.d.ts +12 -0
- package/example/src/main.ts +5 -0
- package/example/src/pennies.ts +7 -0
- package/example/src/style.css +149 -0
- package/example/tsconfig.json +19 -0
- package/example/vite.config.ts +26 -0
- package/jest.config.js +18 -0
- package/package.json +1 -5
- package/scripts/copy-wasm.mjs +21 -0
- package/scripts/smoke.mjs +382 -0
- package/src/address/record-type.ts +77 -0
- package/src/address/wallet-address.ts +105 -0
- package/src/amount.ts +143 -0
- package/src/assert.ts +30 -0
- package/src/capnp/README.md +15 -0
- package/src/capnp/adminNetTx.capnp.ts +66 -0
- package/src/capnp/appLayBlock.capnp.ts +26 -0
- package/src/capnp/appLayLedger.capnp.ts +35 -0
- package/src/capnp/appLayRec.capnp.ts +159 -0
- package/src/capnp/chainStateView.capnp.ts +49 -0
- package/src/capnp/coinAcctRec.capnp.ts +193 -0
- package/src/capnp/genesisBlock.capnp.ts +56 -0
- package/src/capnp/hubContentStmt.capnp.ts +59 -0
- package/src/capnp/misc.capnp.ts +19 -0
- package/src/capnp/netActorRec.capnp.ts +64 -0
- package/src/capnp/nodeContentStmt.capnp.ts +62 -0
- package/src/capnp/nodePayloadList.capnp.ts +28 -0
- package/src/capnp/nodeTxList.capnp.ts +37 -0
- package/src/capnp/paramAcctRec.capnp.ts +669 -0
- package/src/capnp/payload.capnp.ts +261 -0
- package/src/capnp/proxyAcctRec.capnp.ts +259 -0
- package/src/capnp/relayContentStmt.capnp.ts +85 -0
- package/src/capnp/sysAcctRec.capnp.ts +47 -0
- package/src/capnp/userAppTx.capnp.ts +269 -0
- package/src/capnp/userValTx.capnp.ts +1999 -0
- package/src/capnp/valLayBlock.capnp.ts +93 -0
- package/src/capnp/valLayLedger.capnp.ts +80 -0
- package/src/chain/geeq-chain.ts +1094 -0
- package/src/chain/geeq-node.ts +105 -0
- package/src/chain/nonce-guard.ts +73 -0
- package/src/chain/select.ts +149 -0
- package/src/crypto/account-id.ts +83 -0
- package/src/crypto/keypair.ts +221 -0
- package/src/crypto/mnemonic.ts +69 -0
- package/src/encoding/base64.ts +58 -0
- package/src/encoding/hex.ts +42 -0
- package/src/errors.ts +122 -0
- package/src/index.ts +195 -0
- package/src/ledger/coin-accounts.ts +73 -0
- package/src/ledger/decode.ts +147 -0
- package/src/ledger/geeq-ledger.ts +184 -0
- package/src/ledger/net-actors.ts +129 -0
- package/src/ledger/nft-records.ts +49 -0
- package/src/ledger/params.ts +82 -0
- package/src/ledger/proxy-accounts.ts +51 -0
- package/src/net/discovery.ts +231 -0
- package/src/net/explorer.ts +123 -0
- package/src/net/read-ledger.ts +58 -0
- package/src/net/routes.ts +79 -0
- package/src/net/transport.ts +221 -0
- package/src/net/websocket.ts +94 -0
- package/src/runtime/bindings.ts +38 -0
- package/src/runtime/init.ts +49 -0
- package/src/runtime/wasm-loader.browser.ts +22 -0
- package/src/runtime/wasm-loader.node.ts +16 -0
- package/src/socket/geeq-socket.ts +827 -0
- package/src/socket/tracked-tx.ts +319 -0
- package/src/socket/tx-status.ts +131 -0
- package/src/tx/attestation.ts +222 -0
- package/src/tx/content.ts +82 -0
- package/src/tx/encode.ts +107 -0
- package/src/tx/expiry.ts +11 -0
- package/src/tx/fee.ts +31 -0
- package/src/tx/metadata.ts +100 -0
- package/src/tx/nft.ts +108 -0
- package/src/tx/proxy.ts +156 -0
- package/src/tx/transfer.ts +162 -0
- package/src/types.ts +503 -0
- package/src/wasm/README.md +22 -0
- package/src/wasm/browser/geeq_core.d.ts +1224 -0
- package/src/wasm/browser/geeq_core.js +6014 -0
- package/src/wasm/browser/geeq_core_bg.wasm +0 -0
- package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/geeq_core.d.ts +753 -0
- package/src/wasm/nodejs/geeq_core.js +6016 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/package.json +3 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +60 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Message } from 'capnp-ts'
|
|
2
|
+
import { ValLayLedger as CapnpValLayLedger } from '../capnp/valLayLedger.capnp'
|
|
3
|
+
|
|
4
|
+
import { MAX_UINT32 } from '../assert'
|
|
5
|
+
import { fromBase64, toBase64 } from '../encoding/base64'
|
|
6
|
+
import { GeeqError, ledgerUnreadable } from '../errors'
|
|
7
|
+
import type { CoinAccountFacts } from './coin-accounts'
|
|
8
|
+
import { readCoinAccount } from './coin-accounts'
|
|
9
|
+
import { GeeqLedger } from './geeq-ledger'
|
|
10
|
+
import type { GeeqLedgerNode } from './net-actors'
|
|
11
|
+
import { isProcessing, toNodeRole, toNodeStatus } from './net-actors'
|
|
12
|
+
import { readGeeqNorm, readMetadataLimits, readMinimumTransfer } from './params'
|
|
13
|
+
import type { ProxyAccountFacts } from './proxy-accounts'
|
|
14
|
+
import { readProxyAccount } from './proxy-accounts'
|
|
15
|
+
|
|
16
|
+
export interface DecodeLedgerOptions {
|
|
17
|
+
/** The node this came from, carried through onto the snapshot. */
|
|
18
|
+
readonly nodeUrl?: string | undefined
|
|
19
|
+
/** Overrides the read timestamp. For deterministic tests. */
|
|
20
|
+
readonly readAt?: number | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Decode a validation layer ledger.
|
|
25
|
+
*
|
|
26
|
+
* `source` is what a node's `result` field holds: base64, or the bytes behind it. The
|
|
27
|
+
* message is Cap'n Proto **packed**, which is what the node writes with
|
|
28
|
+
* `toPackedArrayBuffer()` — not the flattened form used for signing.
|
|
29
|
+
*
|
|
30
|
+
* Everything is read in this one pass and the message is then dropped, so the caller
|
|
31
|
+
* gets plain values with no traversal budget attached. See {@link GeeqLedger}.
|
|
32
|
+
*/
|
|
33
|
+
export function decodeValidationLedger(
|
|
34
|
+
source: Uint8Array | ArrayBuffer | string,
|
|
35
|
+
options: DecodeLedgerOptions = {},
|
|
36
|
+
): GeeqLedger {
|
|
37
|
+
const bytes =
|
|
38
|
+
typeof source === 'string'
|
|
39
|
+
? fromBase64(source, 'The ledger')
|
|
40
|
+
: source instanceof ArrayBuffer
|
|
41
|
+
? new Uint8Array(source)
|
|
42
|
+
: source
|
|
43
|
+
|
|
44
|
+
if (bytes.length === 0) {
|
|
45
|
+
throw ledgerUnreadable(
|
|
46
|
+
'The ledger is empty. A node reports this before it has committed its first ' +
|
|
47
|
+
'block and while it is resyncing — wait and retry, or read from another node ' +
|
|
48
|
+
'on the same chain.',
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let ledger
|
|
53
|
+
try {
|
|
54
|
+
ledger = new Message(bytes, true).getRoot(CapnpValLayLedger)
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw ledgerUnreadable(
|
|
57
|
+
`Could not decode ${bytes.length} bytes as a packed Cap'n Proto ValLayLedger. ` +
|
|
58
|
+
'The node is probably running a schema version this SDK does not know — ' +
|
|
59
|
+
'upgrade @geeqdev/geeq-sdk, or read the chain state yourself.',
|
|
60
|
+
cause,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const chainNumber = ledger.getChainNum()
|
|
66
|
+
const blockNumber = ledger.getBlockNum()
|
|
67
|
+
|
|
68
|
+
if (blockNumber >= MAX_UINT32) {
|
|
69
|
+
// Every transaction targets blockNumber + 1, which would overflow the uint32 the
|
|
70
|
+
// protocol encodes it in. Refuse rather than silently wrap to zero.
|
|
71
|
+
throw ledgerUnreadable(
|
|
72
|
+
`The ledger reports block ${blockNumber}, the maximum a uint32 can hold, so no ` +
|
|
73
|
+
'transaction can target the next block. The ledger is almost certainly corrupt.',
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nodes: GeeqLedgerNode[] = ledger
|
|
78
|
+
.getNetActorList()
|
|
79
|
+
.getNetActorRecs()
|
|
80
|
+
.toArray()
|
|
81
|
+
.map((record) => {
|
|
82
|
+
const role = toNodeRole(record.getNetActorType())
|
|
83
|
+
const status = toNodeStatus(record.getRecStatus())
|
|
84
|
+
const retiresAtBlock = record.getBlockNumRemove()
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
nodeNumber: record.getNodeNumSelf(),
|
|
87
|
+
host: record.getIpAddr(),
|
|
88
|
+
port: record.getPortNum(),
|
|
89
|
+
role,
|
|
90
|
+
status,
|
|
91
|
+
processing: isProcessing(role, status),
|
|
92
|
+
retiresAtBlock,
|
|
93
|
+
// The node's own guard compares its blockNumRemove against its current block
|
|
94
|
+
// plus two, and the ledger's block is that current block.
|
|
95
|
+
retiring: retiresAtBlock < blockNumber + 2,
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const params = ledger.getParamAcctList().getParamAcctRecs().toArray()
|
|
100
|
+
|
|
101
|
+
// A plausibility gate. Packed capnp can "succeed" on garbage and yield a message of
|
|
102
|
+
// zeros, which would otherwise surface much later as chain 0, block 0 and a missing
|
|
103
|
+
// account. A real ledger always carries net actors and parameters.
|
|
104
|
+
if (nodes.length === 0 || params.length === 0) {
|
|
105
|
+
throw ledgerUnreadable(
|
|
106
|
+
`The ledger decoded but does not look like one: chain ${chainNumber}, block ` +
|
|
107
|
+
`${blockNumber}, ${nodes.length} net actor records, ${params.length} parameter ` +
|
|
108
|
+
'records. A real ledger always has at least one of each, so this URL is ' +
|
|
109
|
+
'probably not a Geeq node.',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const accounts = new Map<string, CoinAccountFacts>()
|
|
114
|
+
const coinAccounts = ledger.getCoinAcctRecs()
|
|
115
|
+
for (let index = 0; index < coinAccounts.getLength(); index += 1) {
|
|
116
|
+
const record = coinAccounts.get(index)
|
|
117
|
+
const accountId = new Uint8Array(record.getAcctNumVal().toArrayBuffer())
|
|
118
|
+
accounts.set(toBase64(accountId), readCoinAccount(record))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const proxies = new Map<string, ProxyAccountFacts>()
|
|
122
|
+
for (const record of ledger.getProxyAcctRecs().toArray()) {
|
|
123
|
+
const accountId = new Uint8Array(record.getAcctNumVal().toArrayBuffer())
|
|
124
|
+
proxies.set(toBase64(accountId), readProxyAccount(record))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new GeeqLedger({
|
|
128
|
+
chainNumber,
|
|
129
|
+
blockNumber,
|
|
130
|
+
nodes,
|
|
131
|
+
metadataLimits: readMetadataLimits(params),
|
|
132
|
+
minimumTransfer: readMinimumTransfer(params),
|
|
133
|
+
geeqNorm: readGeeqNorm(params),
|
|
134
|
+
accounts,
|
|
135
|
+
proxies,
|
|
136
|
+
readAt: options.readAt ?? Date.now(),
|
|
137
|
+
nodeUrl: options.nodeUrl,
|
|
138
|
+
})
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
if (cause instanceof GeeqError) throw cause
|
|
141
|
+
throw ledgerUnreadable(
|
|
142
|
+
'The ledger decoded but its contents could not be read. The node is probably ' +
|
|
143
|
+
'running a schema version this SDK does not know.',
|
|
144
|
+
cause,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { resolveAccountId } from '../crypto/account-id'
|
|
2
|
+
import type { AccountRef } from '../crypto/account-id'
|
|
3
|
+
import { fromBase64, toBase64 } from '../encoding/base64'
|
|
4
|
+
import type { GeeqMetadataLimits } from '../types'
|
|
5
|
+
import type { CoinAccountFacts, GeeqCoinAccount } from './coin-accounts'
|
|
6
|
+
import type { GeeqLedgerNode } from './net-actors'
|
|
7
|
+
import type { GeeqProxyAccount, ProxyAccountFacts } from './proxy-accounts'
|
|
8
|
+
|
|
9
|
+
/** @internal — built by {@link decodeValidationLedger}, never by hand. */
|
|
10
|
+
export interface GeeqLedgerInit {
|
|
11
|
+
readonly chainNumber: number
|
|
12
|
+
readonly blockNumber: number
|
|
13
|
+
readonly nodes: readonly GeeqLedgerNode[]
|
|
14
|
+
readonly metadataLimits: GeeqMetadataLimits | undefined
|
|
15
|
+
readonly minimumTransfer: bigint | undefined
|
|
16
|
+
readonly geeqNorm: bigint | undefined
|
|
17
|
+
readonly accounts: ReadonlyMap<string, CoinAccountFacts>
|
|
18
|
+
readonly proxies: ReadonlyMap<string, ProxyAccountFacts>
|
|
19
|
+
readonly readAt: number
|
|
20
|
+
readonly nodeUrl: string | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One validation layer ledger, decoded into plain values.
|
|
25
|
+
*
|
|
26
|
+
* This is a snapshot, not a view: the Cap'n Proto `Message` it came from is released
|
|
27
|
+
* the moment decoding finishes. That is deliberate and load-bearing. capnp-ts counts a
|
|
28
|
+
* traversal budget per `Message` and never resets it, so a retained handle rescanned on
|
|
29
|
+
* every transaction eventually throws `PTR_TRAVERSAL_LIMIT_EXCEEDED`; and reading a
|
|
30
|
+
* malformed union arm mutates the message in place. Neither can happen to a snapshot.
|
|
31
|
+
*
|
|
32
|
+
* It holds no WebAssembly handle, so unlike a keypair it needs no `dispose()`.
|
|
33
|
+
*/
|
|
34
|
+
export class GeeqLedger {
|
|
35
|
+
/** The ledger's own chain number. A transaction must name exactly this. */
|
|
36
|
+
readonly chainNumber: number
|
|
37
|
+
/** N — the last block committed to this ledger. */
|
|
38
|
+
readonly blockNumber: number
|
|
39
|
+
/** N + 1 — the block a transaction built now should ask for. */
|
|
40
|
+
readonly targetBlockNumber: number
|
|
41
|
+
/** Every net actor, in ledger order. */
|
|
42
|
+
readonly nodes: readonly GeeqLedgerNode[]
|
|
43
|
+
/** Undefined when this chain publishes no metadata parameters. */
|
|
44
|
+
readonly metadataLimits: GeeqMetadataLimits | undefined
|
|
45
|
+
/**
|
|
46
|
+
* The smallest transfer this chain accepts, in pennies.
|
|
47
|
+
*
|
|
48
|
+
* Undefined when the chain does not publish the parameters it is derived from.
|
|
49
|
+
*/
|
|
50
|
+
readonly minimumTransfer: bigint | undefined
|
|
51
|
+
/**
|
|
52
|
+
* The chain's `geeqNorm` parameter, which the minimum transfer and every fee are
|
|
53
|
+
* multiples of. Undefined when the chain does not publish it.
|
|
54
|
+
*/
|
|
55
|
+
readonly geeqNorm: bigint | undefined
|
|
56
|
+
/** `Date.now()` when this was read, so callers can judge staleness. */
|
|
57
|
+
readonly readAt: number
|
|
58
|
+
/** The node it was read from, when it came from one. */
|
|
59
|
+
readonly nodeUrl: string | undefined
|
|
60
|
+
|
|
61
|
+
// Deliberately a TypeScript `private`, not a `#private` field. A `#private` is
|
|
62
|
+
// unreachable through a Proxy, which breaks Vue's ref() — the same trap documented
|
|
63
|
+
// on Keypair in src/crypto/keypair.ts. Non-enumerable so it stays out of logs and
|
|
64
|
+
// structured clones, since on a busy chain it is the largest thing here.
|
|
65
|
+
private readonly accounts!: ReadonlyMap<string, CoinAccountFacts>
|
|
66
|
+
private readonly proxies!: ReadonlyMap<string, ProxyAccountFacts>
|
|
67
|
+
|
|
68
|
+
/** @internal */
|
|
69
|
+
constructor(init: GeeqLedgerInit) {
|
|
70
|
+
this.chainNumber = init.chainNumber
|
|
71
|
+
this.blockNumber = init.blockNumber
|
|
72
|
+
this.targetBlockNumber = init.blockNumber + 1
|
|
73
|
+
this.nodes = Object.freeze([...init.nodes])
|
|
74
|
+
this.metadataLimits = init.metadataLimits
|
|
75
|
+
this.minimumTransfer = init.minimumTransfer
|
|
76
|
+
this.geeqNorm = init.geeqNorm
|
|
77
|
+
this.readAt = init.readAt
|
|
78
|
+
this.nodeUrl = init.nodeUrl
|
|
79
|
+
Object.defineProperty(this, 'accounts', {
|
|
80
|
+
value: init.accounts,
|
|
81
|
+
enumerable: false,
|
|
82
|
+
writable: false,
|
|
83
|
+
})
|
|
84
|
+
Object.defineProperty(this, 'proxies', {
|
|
85
|
+
value: init.proxies,
|
|
86
|
+
enumerable: false,
|
|
87
|
+
writable: false,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The nodes that will actually accept a transaction. */
|
|
92
|
+
get processingNodes(): readonly GeeqLedgerNode[] {
|
|
93
|
+
return this.nodes.filter((node) => node.processing)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find a net actor record.
|
|
98
|
+
*
|
|
99
|
+
* By `host` this matches the bare IP and nothing else — every first-party client does
|
|
100
|
+
* the same, because a record's `port` is the node's internal listen port while clients
|
|
101
|
+
* connect through a proxy on another one. Matching on both finds nothing.
|
|
102
|
+
*/
|
|
103
|
+
findNode(match: { readonly host: string } | { readonly nodeNumber: number }): GeeqLedgerNode | undefined {
|
|
104
|
+
if ('nodeNumber' in match) {
|
|
105
|
+
return this.nodes.find((node) => node.nodeNumber === match.nodeNumber)
|
|
106
|
+
}
|
|
107
|
+
return this.nodes.find((node) => node.host === match.host)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Every net actor record at one host. More than one is possible, and ambiguous. */
|
|
111
|
+
findNodes(host: string): readonly GeeqLedgerNode[] {
|
|
112
|
+
return this.nodes.filter((node) => node.host === host)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The signing account's coin record, or `undefined` when this chain has none for it.
|
|
117
|
+
*
|
|
118
|
+
* Absence is the ordinary state of a freshly generated keypair: an account appears in
|
|
119
|
+
* the ledger only once it has been funded.
|
|
120
|
+
*/
|
|
121
|
+
findCoinAccount(account: AccountRef): GeeqCoinAccount | undefined {
|
|
122
|
+
const accountId = resolveAccountId(account)
|
|
123
|
+
const facts = this.accounts.get(toBase64(accountId))
|
|
124
|
+
if (facts === undefined) return undefined
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
accountId,
|
|
127
|
+
kind: facts.kind,
|
|
128
|
+
nonce: facts.nonce,
|
|
129
|
+
balance: facts.balance,
|
|
130
|
+
nfts: facts.nfts,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A proxy record, or `undefined` when this chain has none with that id. */
|
|
135
|
+
findProxyAccount(account: AccountRef): GeeqProxyAccount | undefined {
|
|
136
|
+
const accountId = resolveAccountId(account)
|
|
137
|
+
const facts = this.proxies.get(toBase64(accountId))
|
|
138
|
+
if (facts === undefined) return undefined
|
|
139
|
+
return Object.freeze({ accountId, ...facts })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every NFT mint proxy an account administers — the ones it can mint through.
|
|
144
|
+
*
|
|
145
|
+
* Found by the admin each record names, so it needs nothing remembered from when they
|
|
146
|
+
* were created.
|
|
147
|
+
*/
|
|
148
|
+
findProxyAccounts(match: { readonly admin: AccountRef }): readonly GeeqProxyAccount[] {
|
|
149
|
+
const admin = toBase64(resolveAccountId(match.admin))
|
|
150
|
+
const found: GeeqProxyAccount[] = []
|
|
151
|
+
for (const [key, facts] of this.proxies) {
|
|
152
|
+
if (facts.admin !== undefined && toBase64(facts.admin) === admin) {
|
|
153
|
+
found.push(Object.freeze({ accountId: fromBase64(key), ...facts }))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return found
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** How many coin accounts this ledger carries. Mostly useful for diagnostics. */
|
|
160
|
+
get coinAccountCount(): number {
|
|
161
|
+
return this.accounts.size
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Summary only — the account table is far too large to serialise. */
|
|
165
|
+
toJSON(): Record<string, unknown> {
|
|
166
|
+
return {
|
|
167
|
+
chainNumber: this.chainNumber,
|
|
168
|
+
blockNumber: this.blockNumber,
|
|
169
|
+
targetBlockNumber: this.targetBlockNumber,
|
|
170
|
+
nodes: this.nodes,
|
|
171
|
+
metadataLimits: this.metadataLimits,
|
|
172
|
+
minimumTransfer: this.minimumTransfer?.toString(),
|
|
173
|
+
geeqNorm: this.geeqNorm?.toString(),
|
|
174
|
+
coinAccountCount: this.coinAccountCount,
|
|
175
|
+
proxyAccountCount: this.proxies.size,
|
|
176
|
+
readAt: this.readAt,
|
|
177
|
+
nodeUrl: this.nodeUrl,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
toString(): string {
|
|
182
|
+
return `GeeqLedger(chain ${this.chainNumber}, block ${this.blockNumber}, ${this.nodes.length} nodes)`
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NetActorStatus as CapnpNetActorStatus,
|
|
3
|
+
NetActorType as CapnpNetActorType,
|
|
4
|
+
} from '../capnp/netActorRec.capnp'
|
|
5
|
+
|
|
6
|
+
/** What kind of actor a net actor record describes. */
|
|
7
|
+
export type GeeqNodeRole =
|
|
8
|
+
| 'anchor-node'
|
|
9
|
+
| 'anon-node'
|
|
10
|
+
| 'cert-node'
|
|
11
|
+
| 'cert-authority'
|
|
12
|
+
| 'chain-admin'
|
|
13
|
+
| 'relay'
|
|
14
|
+
| 'treasury-agent'
|
|
15
|
+
| 'unknown'
|
|
16
|
+
|
|
17
|
+
/** What that actor is currently doing. */
|
|
18
|
+
export type GeeqNodeStatus =
|
|
19
|
+
| 'chain-admin'
|
|
20
|
+
| 'hub'
|
|
21
|
+
| 'leaving'
|
|
22
|
+
| 'listening'
|
|
23
|
+
| 'node'
|
|
24
|
+
| 'ready'
|
|
25
|
+
| 'relay'
|
|
26
|
+
| 'suspended'
|
|
27
|
+
| 'unknown'
|
|
28
|
+
|
|
29
|
+
const ROLES: Readonly<Record<number, GeeqNodeRole>> = Object.freeze({
|
|
30
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_ANCHOR_NODE]: 'anchor-node',
|
|
31
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_ANON_NODE]: 'anon-node',
|
|
32
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_CERT_NODE]: 'cert-node',
|
|
33
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_CERT_AUTH]: 'cert-authority',
|
|
34
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_CHAIN_ADMIN]: 'chain-admin',
|
|
35
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_RELAY]: 'relay',
|
|
36
|
+
[CapnpNetActorType.NET_ACTOR_TYPE_TREASURY_AGENT]: 'treasury-agent',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const STATUSES: Readonly<Record<number, GeeqNodeStatus>> = Object.freeze({
|
|
40
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_CHAIN_ADMIN]: 'chain-admin',
|
|
41
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_HUB]: 'hub',
|
|
42
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_LEAVING]: 'leaving',
|
|
43
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_LISTENING]: 'listening',
|
|
44
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_NODE]: 'node',
|
|
45
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_READY]: 'ready',
|
|
46
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_RELAY]: 'relay',
|
|
47
|
+
[CapnpNetActorStatus.NET_ACTOR_STATUS_SUSPENDED]: 'suspended',
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Unknown values map to `'unknown'` rather than throwing.
|
|
52
|
+
*
|
|
53
|
+
* This deliberately differs from `fromWasmRecordType`, which throws: a bad record type
|
|
54
|
+
* means a corrupt address, but a new actor kind on a newer chain must not make an
|
|
55
|
+
* otherwise-valid ledger undecodable. An `'unknown'` actor is never `processing`, so
|
|
56
|
+
* the safe outcome is the default.
|
|
57
|
+
*/
|
|
58
|
+
export function toNodeRole(value: number): GeeqNodeRole {
|
|
59
|
+
return ROLES[value] ?? 'unknown'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function toNodeStatus(value: number): GeeqNodeStatus {
|
|
63
|
+
return STATUSES[value] ?? 'unknown'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Roles that run the transaction handler at all. */
|
|
67
|
+
const PROCESSING_ROLES: ReadonlySet<GeeqNodeRole> = new Set<GeeqNodeRole>([
|
|
68
|
+
'anon-node',
|
|
69
|
+
'anchor-node',
|
|
70
|
+
'cert-node',
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
/** Statuses in which such a role actually accepts transactions. */
|
|
74
|
+
const PROCESSING_STATUSES: ReadonlySet<GeeqNodeStatus> = new Set<GeeqNodeStatus>(['node', 'hub'])
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether this actor will accept a transaction.
|
|
78
|
+
*
|
|
79
|
+
* Mirrors `isActive()` in the node itself
|
|
80
|
+
* (geeq_node_core/src/wrappers/network-actor-record/index.ts). Anything else answers
|
|
81
|
+
* HTTP 400 `'not processing'`, or — for a relay — registers no handler at all.
|
|
82
|
+
*/
|
|
83
|
+
export function isProcessing(role: GeeqNodeRole, status: GeeqNodeStatus): boolean {
|
|
84
|
+
return PROCESSING_ROLES.has(role) && PROCESSING_STATUSES.has(status)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One net actor record, as plain values. */
|
|
88
|
+
export interface GeeqLedgerNode {
|
|
89
|
+
/** `nodeNumSelf` — what a transaction must name, checked with strict equality. */
|
|
90
|
+
readonly nodeNumber: number
|
|
91
|
+
/** The bare IP the ledger records. Nodes are matched on this and nothing else. */
|
|
92
|
+
readonly host: string
|
|
93
|
+
/**
|
|
94
|
+
* The port recorded against this node in the ledger.
|
|
95
|
+
*
|
|
96
|
+
* **Not the port you connect to.** Clients always reach a node on 2082, where TLS is
|
|
97
|
+
* terminated; every first-party client ignores this field and rebuilds the URL from
|
|
98
|
+
* its own configuration. Exposed for diagnostics, and used only to break a tie when
|
|
99
|
+
* one address carries several records.
|
|
100
|
+
*/
|
|
101
|
+
readonly port: number
|
|
102
|
+
readonly role: GeeqNodeRole
|
|
103
|
+
readonly status: GeeqNodeStatus
|
|
104
|
+
/**
|
|
105
|
+
* True when the protocol permits this node to accept transactions.
|
|
106
|
+
*
|
|
107
|
+
* **Not a liveness signal.** Nothing in the protocol evicts a crashed node from the
|
|
108
|
+
* net actor list, and `blockNumLiveness` is rewritten for every actor on every commit
|
|
109
|
+
* rather than per node, so it carries no health information at all. A node that died
|
|
110
|
+
* an hour ago still reads as processing here. The only way to learn a node is up is
|
|
111
|
+
* to talk to it.
|
|
112
|
+
*/
|
|
113
|
+
readonly processing: boolean
|
|
114
|
+
/**
|
|
115
|
+
* `blockNumRemove` — the block at which this node's turn in the active set ends.
|
|
116
|
+
*
|
|
117
|
+
* Reset 100–355 blocks ahead each time the node is promoted, so it is a rotation
|
|
118
|
+
* deadline rather than a departure flag.
|
|
119
|
+
*/
|
|
120
|
+
readonly retiresAtBlock: number
|
|
121
|
+
/**
|
|
122
|
+
* True when this node is inside its own shutdown window and will refuse work.
|
|
123
|
+
*
|
|
124
|
+
* The node's submit handler answers HTTP 503 `'Block chain is stopped'` once its
|
|
125
|
+
* `blockNumRemove` is less than its current block plus two, so this is knowable from
|
|
126
|
+
* the ledger before a transaction is ever built.
|
|
127
|
+
*/
|
|
128
|
+
readonly retiring: boolean
|
|
129
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Message } from 'capnp-ts'
|
|
2
|
+
import { AppLayLedger as CapnpAppLayLedger } from '../capnp/appLayLedger.capnp'
|
|
3
|
+
|
|
4
|
+
import { fromBase64, toBase64 } from '../encoding/base64'
|
|
5
|
+
import { ledgerUnreadable } from '../errors'
|
|
6
|
+
|
|
7
|
+
/** What the application layer ledger says one NFT stands for. */
|
|
8
|
+
export interface NftRecordFacts {
|
|
9
|
+
readonly attestHash: Uint8Array
|
|
10
|
+
readonly metadata: readonly string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The NFT records in an application layer ledger, keyed by the NFT's base64 id.
|
|
15
|
+
*
|
|
16
|
+
* `source` is what a node's `result` holds for `type=all`: base64 packed Cap'n Proto, the
|
|
17
|
+
* same form as the validation layer ledger. Other application records are skipped. As
|
|
18
|
+
* with that ledger, the message is dropped once read.
|
|
19
|
+
*/
|
|
20
|
+
export function decodeNftRecords(source: string): Map<string, NftRecordFacts> {
|
|
21
|
+
const bytes = fromBase64(source, 'The application layer ledger')
|
|
22
|
+
const records = new Map<string, NftRecordFacts>()
|
|
23
|
+
const decoder = new TextDecoder()
|
|
24
|
+
try {
|
|
25
|
+
const ledger = new Message(bytes, true).getRoot(CapnpAppLayLedger)
|
|
26
|
+
for (const record of ledger.getAppLayRecs().toArray()) {
|
|
27
|
+
// `counterparty` is the union's zero value, so the arm is checked before it is read.
|
|
28
|
+
const recType = record.getRecType()
|
|
29
|
+
if (!recType.isNft() || !recType.hasNft()) continue
|
|
30
|
+
const data = recType.getNft().getAttestationSubgroup().getData()
|
|
31
|
+
const metadata = data
|
|
32
|
+
.getMetadata()
|
|
33
|
+
.toArray()
|
|
34
|
+
.sort((a, b) => a.getPosition() - b.getPosition())
|
|
35
|
+
.map((entry) => decoder.decode(new Uint8Array(entry.getItem().toArrayBuffer())))
|
|
36
|
+
records.set(toBase64(new Uint8Array(record.getAcctNumApp().toArrayBuffer())), {
|
|
37
|
+
attestHash: new Uint8Array(data.getAttestHash().toArrayBuffer()),
|
|
38
|
+
metadata,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
} catch (cause) {
|
|
42
|
+
throw ledgerUnreadable(
|
|
43
|
+
'The application layer ledger could not be read. The node is probably running a ' +
|
|
44
|
+
'schema version this SDK does not know.',
|
|
45
|
+
cause,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return records
|
|
49
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ParamAcctRec } from '../capnp/paramAcctRec.capnp'
|
|
2
|
+
|
|
3
|
+
import { uint64ToPennies } from '../amount'
|
|
4
|
+
import type { GeeqMetadataLimits } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read the metadata limits a chain publishes in its parameter account list.
|
|
8
|
+
*
|
|
9
|
+
* `metadataVarNum` caps the NUMBER of entries and `metadataVarBytes` caps the bytes in
|
|
10
|
+
* ONE entry; there is no aggregate cap. The node enforces both at submit time, so
|
|
11
|
+
* having them here is what lets a transaction be rejected before it is signed rather
|
|
12
|
+
* than after it is sent.
|
|
13
|
+
*
|
|
14
|
+
* Returns `undefined` when either parameter is absent. A chain that does not publish
|
|
15
|
+
* them is a schema difference, not a caller error, so the preflight is skipped and the
|
|
16
|
+
* node stays the authority.
|
|
17
|
+
*
|
|
18
|
+
* `metadataFixedNum` / `metadataFixedBytes` exist in the schema but are read nowhere in
|
|
19
|
+
* the current node, so they are ignored here too.
|
|
20
|
+
*/
|
|
21
|
+
export function readMetadataLimits(
|
|
22
|
+
records: readonly ParamAcctRec[],
|
|
23
|
+
): GeeqMetadataLimits | undefined {
|
|
24
|
+
let maxEntries: number | undefined
|
|
25
|
+
let maxBytesPerEntry: number | undefined
|
|
26
|
+
|
|
27
|
+
for (const record of records) {
|
|
28
|
+
const param = record.getParamType()
|
|
29
|
+
if (maxEntries === undefined && param.isMetadataVarNum() && param.hasMetadataVarNum()) {
|
|
30
|
+
maxEntries = param.getMetadataVarNum().getCurrentValue()
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
maxBytesPerEntry === undefined &&
|
|
35
|
+
param.isMetadataVarBytes() &&
|
|
36
|
+
param.hasMetadataVarBytes()
|
|
37
|
+
) {
|
|
38
|
+
maxBytesPerEntry = param.getMetadataVarBytes().getCurrentValue()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (maxEntries === undefined || maxBytesPerEntry === undefined) return undefined
|
|
43
|
+
return Object.freeze({ maxEntries, maxBytesPerEntry })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The smallest transfer this chain will accept, in pennies: `minTxAmt × geeqNorm + 1`.
|
|
48
|
+
*
|
|
49
|
+
* The node rejects anything below `minTxAmt × geeqNorm` (geeq_node_core, Transaction
|
|
50
|
+
* Amount Conformity); the Geeq wallet asks for one penny more, and so does this. Knowing
|
|
51
|
+
* it here turns a rejected transaction into a message before it is signed.
|
|
52
|
+
*
|
|
53
|
+
* Returns `undefined` when either parameter is missing, in which case nothing is checked
|
|
54
|
+
* and the node stays the authority.
|
|
55
|
+
*/
|
|
56
|
+
export function readMinimumTransfer(
|
|
57
|
+
records: readonly ParamAcctRec[],
|
|
58
|
+
): bigint | undefined {
|
|
59
|
+
const geeqNorm = readGeeqNorm(records)
|
|
60
|
+
for (const record of records) {
|
|
61
|
+
const param = record.getParamType()
|
|
62
|
+
if (param.isMinTxAmt() && param.hasMinTxAmt()) {
|
|
63
|
+
if (geeqNorm === undefined) return undefined
|
|
64
|
+
return uint64ToPennies(param.getMinTxAmt().getCurrentValue()) * geeqNorm + 1n
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The chain's `geeqNorm` parameter: the multiplier behind its minimum transfer and every
|
|
72
|
+
* fee. `undefined` when the chain does not publish it.
|
|
73
|
+
*/
|
|
74
|
+
export function readGeeqNorm(records: readonly ParamAcctRec[]): bigint | undefined {
|
|
75
|
+
for (const record of records) {
|
|
76
|
+
const param = record.getParamType()
|
|
77
|
+
if (param.isGeeqNorm() && param.hasGeeqNorm()) {
|
|
78
|
+
return uint64ToPennies(param.getGeeqNorm().getCurrentValue())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ProxyAcctRec } from '../capnp/proxyAcctRec.capnp'
|
|
2
|
+
|
|
3
|
+
import { uint64ToPennies } from '../amount'
|
|
4
|
+
|
|
5
|
+
/** Which kind of proxy a record is. Only `'nft-mint'` is read further. */
|
|
6
|
+
export type GeeqProxyKind = 'nft-mint' | 'token-mint' | 'funding' | 'multisig' | 'dce' | 'unknown'
|
|
7
|
+
|
|
8
|
+
/** One proxy account record, as plain values. */
|
|
9
|
+
export interface GeeqProxyAccount {
|
|
10
|
+
readonly accountId: Uint8Array
|
|
11
|
+
readonly kind: GeeqProxyKind
|
|
12
|
+
/**
|
|
13
|
+
* The proxy's own nonce. A mint through it spends this one, not the admin's — a proxy
|
|
14
|
+
* carries its nonce on the record itself.
|
|
15
|
+
*/
|
|
16
|
+
readonly nonce: number
|
|
17
|
+
/** Balance in pennies. Use `formatGeeq` to show it. */
|
|
18
|
+
readonly balance: bigint
|
|
19
|
+
/** The account id of the only key that can sign for it. Read for NFT mint proxies only. */
|
|
20
|
+
readonly admin: Uint8Array | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What we keep per proxy. The id is the map key, so it is not repeated here. */
|
|
24
|
+
export type ProxyAccountFacts = Omit<GeeqProxyAccount, 'accountId'>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a proxy record's kind and admin.
|
|
28
|
+
*
|
|
29
|
+
* The same zero-value trap as a coin account: `DCE` is the union's zero value, so every
|
|
30
|
+
* arm is checked with `isX()` before anything is read from it.
|
|
31
|
+
*/
|
|
32
|
+
export function readProxyAccount(record: ProxyAcctRec): ProxyAccountFacts {
|
|
33
|
+
const recType = record.getRecType()
|
|
34
|
+
const nonce = record.getNonce()
|
|
35
|
+
const balance = uint64ToPennies(record.getAmtCoin())
|
|
36
|
+
|
|
37
|
+
if (recType.isNftMint() && recType.hasNftMint()) {
|
|
38
|
+
const admin = new Uint8Array(recType.getNftMint().getAcctNumAdminUser().toArrayBuffer())
|
|
39
|
+
return { kind: 'nft-mint', nonce, balance, admin }
|
|
40
|
+
}
|
|
41
|
+
const kind = recType.isTokenMint()
|
|
42
|
+
? 'token-mint'
|
|
43
|
+
: recType.isFunding()
|
|
44
|
+
? 'funding'
|
|
45
|
+
: recType.isMultisig()
|
|
46
|
+
? 'multisig'
|
|
47
|
+
: recType.isDce() && recType.hasDce()
|
|
48
|
+
? 'dce'
|
|
49
|
+
: 'unknown'
|
|
50
|
+
return { kind, nonce, balance, admin: undefined }
|
|
51
|
+
}
|