@geeqdev/geeq-sdk 0.0.1 → 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,231 @@
|
|
|
1
|
+
import { invalidArgument, nodeUnusable } from '../errors'
|
|
2
|
+
import { GEEQ_NODE_PORT } from '../chain/select'
|
|
3
|
+
import { chainsRoute, nodesRoute } from './routes'
|
|
4
|
+
import type { RequestOptions } from './transport'
|
|
5
|
+
import { requestText } from './transport'
|
|
6
|
+
|
|
7
|
+
/** How the directory classifies a node. Only `'active'` processes transactions. */
|
|
8
|
+
export type GeeqNodeKind = 'active' | 'relay' | 'listener' | 'chain-admin' | 'none' | 'unknown'
|
|
9
|
+
|
|
10
|
+
/** One chain the directory knows about. */
|
|
11
|
+
export interface GeeqDiscoveredChain {
|
|
12
|
+
/** The directory's own row id, which every other directory route calls `systemId`. */
|
|
13
|
+
readonly systemId: number
|
|
14
|
+
/** The protocol chain number — what a transaction carries and a node checks. */
|
|
15
|
+
readonly chainNumber: number
|
|
16
|
+
readonly name: string | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One node the directory knows about.
|
|
21
|
+
*
|
|
22
|
+
* Note what is **missing**: a node number. The directory can say where a node is but
|
|
23
|
+
* not who it is in the chain's own net actor list, so the ledger read is still the only
|
|
24
|
+
* way to learn the number a transaction must carry.
|
|
25
|
+
*/
|
|
26
|
+
export interface GeeqDiscoveredNode {
|
|
27
|
+
readonly host: string
|
|
28
|
+
readonly kind: GeeqNodeKind
|
|
29
|
+
/** The directory's own lifecycle state — `new`, `ready`, `stopped`. */
|
|
30
|
+
readonly status: string
|
|
31
|
+
/** Where to reach it. Always the client port; the directory's own port is internal. */
|
|
32
|
+
readonly url: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const KINDS: ReadonlySet<string> = new Set([
|
|
36
|
+
'active',
|
|
37
|
+
'relay',
|
|
38
|
+
'listener',
|
|
39
|
+
'chain-admin',
|
|
40
|
+
'none',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
function resultOf(body: string, url: string): unknown[] {
|
|
44
|
+
let parsed: unknown
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(body)
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
throw nodeUnusable(
|
|
49
|
+
`The directory at ${url} did not answer with JSON. A proxy or login page in front ` +
|
|
50
|
+
'of it is the usual reason.',
|
|
51
|
+
cause,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
const result = (parsed as { result?: unknown })?.result
|
|
55
|
+
if (!Array.isArray(result)) {
|
|
56
|
+
throw nodeUnusable(
|
|
57
|
+
`The directory at ${url} answered without a "result" array, so it does not look ` +
|
|
58
|
+
'like a Geeq node directory.',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Every chain the directory knows about. */
|
|
65
|
+
export async function listChains(
|
|
66
|
+
origin: string,
|
|
67
|
+
request: RequestOptions,
|
|
68
|
+
): Promise<GeeqDiscoveredChain[]> {
|
|
69
|
+
const url = chainsRoute(origin)
|
|
70
|
+
const rows = resultOf(await requestText(url, request), url)
|
|
71
|
+
|
|
72
|
+
return rows
|
|
73
|
+
.map((row) => row as { id?: unknown; chainNumber?: unknown; name?: unknown })
|
|
74
|
+
.filter((row) => typeof row.id === 'number' && typeof row.chainNumber === 'number')
|
|
75
|
+
.map((row) =>
|
|
76
|
+
Object.freeze({
|
|
77
|
+
systemId: row.id as number,
|
|
78
|
+
chainNumber: row.chainNumber as number,
|
|
79
|
+
name: typeof row.name === 'string' ? row.name : undefined,
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface NodeAddressing {
|
|
85
|
+
/** Scheme for the node URLs. Defaults to the directory's own. */
|
|
86
|
+
readonly protocol?: string | undefined
|
|
87
|
+
/** Client port. Defaults to {@link GEEQ_NODE_PORT}; override only for local setups. */
|
|
88
|
+
readonly port?: number | undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every node the directory lists for one chain. */
|
|
92
|
+
export async function listNodes(
|
|
93
|
+
origin: string,
|
|
94
|
+
systemId: number,
|
|
95
|
+
request: RequestOptions,
|
|
96
|
+
addressing: NodeAddressing = {},
|
|
97
|
+
): Promise<GeeqDiscoveredNode[]> {
|
|
98
|
+
const protocol = addressing.protocol ?? new URL(origin).protocol
|
|
99
|
+
const port = addressing.port ?? GEEQ_NODE_PORT
|
|
100
|
+
const url = nodesRoute(origin, systemId)
|
|
101
|
+
const rows = resultOf(await requestText(url, request), url)
|
|
102
|
+
|
|
103
|
+
return rows
|
|
104
|
+
.map((row) => row as { ip?: unknown; type?: unknown; status?: unknown })
|
|
105
|
+
.filter((row) => typeof row.ip === 'string' && row.ip !== '')
|
|
106
|
+
.map((row) => {
|
|
107
|
+
const kind = typeof row.type === 'string' && KINDS.has(row.type) ? row.type : 'unknown'
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
host: row.ip as string,
|
|
110
|
+
kind: kind as GeeqNodeKind,
|
|
111
|
+
status: typeof row.status === 'string' ? row.status : 'unknown',
|
|
112
|
+
// The directory reports the node's own internal listener, which is not where
|
|
113
|
+
// clients connect, so its port field is deliberately ignored in favour of the
|
|
114
|
+
// one port every node answers on.
|
|
115
|
+
url: `${protocol}//${row.ip as string}:${port}`,
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DiscoverOptions extends NodeAddressing {
|
|
121
|
+
readonly origin: string
|
|
122
|
+
/** Select the chain by the name the directory gives it. Matched case-insensitively. */
|
|
123
|
+
readonly chainName?: string | undefined
|
|
124
|
+
/** Select by protocol chain number, or — alongside `chainName` — assert it. */
|
|
125
|
+
readonly chainNumber?: number | undefined
|
|
126
|
+
readonly request: RequestOptions
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Discovery {
|
|
130
|
+
readonly chain: GeeqDiscoveredChain
|
|
131
|
+
/** Every node listed for the chain, in directory order. */
|
|
132
|
+
readonly nodes: readonly GeeqDiscoveredNode[]
|
|
133
|
+
/** Those that will actually take a transaction. */
|
|
134
|
+
readonly usable: readonly GeeqDiscoveredNode[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True when the directory says this node is an active one that has not been stopped. */
|
|
138
|
+
export function isUsable(node: GeeqDiscoveredNode): boolean {
|
|
139
|
+
return node.kind === 'active' && node.status !== 'stopped'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find a chain in the directory and list its nodes.
|
|
144
|
+
*
|
|
145
|
+
* This reads two routes and nothing else. The directory is a deployment control plane:
|
|
146
|
+
* it has no authentication, and its other routes create and destroy machines and serve
|
|
147
|
+
* private keys. Treat every value it returns as a hint about *where* to look, never as
|
|
148
|
+
* authority about the chain itself — that comes from the ledger.
|
|
149
|
+
*/
|
|
150
|
+
export async function discover(options: DiscoverOptions): Promise<Discovery> {
|
|
151
|
+
const chains = await listChains(options.origin, options.request)
|
|
152
|
+
|
|
153
|
+
if (chains.length === 0) {
|
|
154
|
+
throw nodeUnusable(`The directory at ${options.origin} lists no chains.`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const listed = chains
|
|
158
|
+
.map((c) => `${c.name ?? '(unnamed)'} (chain ${c.chainNumber})`)
|
|
159
|
+
.join(', ')
|
|
160
|
+
|
|
161
|
+
let chain: GeeqDiscoveredChain | undefined
|
|
162
|
+
|
|
163
|
+
if (options.chainName !== undefined) {
|
|
164
|
+
// Names are what a person types, so match them the way a person means them.
|
|
165
|
+
const wanted = options.chainName.trim().toLowerCase()
|
|
166
|
+
const matches = chains.filter((c) => c.name?.trim().toLowerCase() === wanted)
|
|
167
|
+
|
|
168
|
+
if (matches.length === 0) {
|
|
169
|
+
throw invalidArgument(
|
|
170
|
+
`The directory at ${options.origin} has no chain named ` +
|
|
171
|
+
`${JSON.stringify(options.chainName)}. Available: ${listed}.`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
if (matches.length > 1) {
|
|
175
|
+
throw invalidArgument(
|
|
176
|
+
`The directory at ${options.origin} lists ${matches.length} chains named ` +
|
|
177
|
+
`${JSON.stringify(options.chainName)} (numbers ` +
|
|
178
|
+
`${matches.map((c) => c.chainNumber).join(', ')}), so the name does not identify ` +
|
|
179
|
+
'one. Select it by chainNumber instead.',
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
chain = matches[0]
|
|
183
|
+
} else if (options.chainNumber !== undefined) {
|
|
184
|
+
chain = chains.find((c) => c.chainNumber === options.chainNumber)
|
|
185
|
+
if (chain === undefined) {
|
|
186
|
+
throw invalidArgument(
|
|
187
|
+
`The directory at ${options.origin} does not list chain ${options.chainNumber}. ` +
|
|
188
|
+
`Available: ${listed}.`,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
if (chains.length > 1) {
|
|
193
|
+
throw invalidArgument(
|
|
194
|
+
`The directory at ${options.origin} lists ${chains.length} chains, so chainName ` +
|
|
195
|
+
`or chainNumber is required to choose one. Available: ${listed}.`,
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
chain = chains[0]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const resolved = chain as GeeqDiscoveredChain
|
|
202
|
+
|
|
203
|
+
// A name and a number together are an assertion, not two ways of choosing.
|
|
204
|
+
if (
|
|
205
|
+
options.chainName !== undefined &&
|
|
206
|
+
options.chainNumber !== undefined &&
|
|
207
|
+
resolved.chainNumber !== options.chainNumber
|
|
208
|
+
) {
|
|
209
|
+
throw invalidArgument(
|
|
210
|
+
`The directory at ${options.origin} says ${JSON.stringify(options.chainName)} is ` +
|
|
211
|
+
`chain ${resolved.chainNumber}, but chainNumber was given as ` +
|
|
212
|
+
`${options.chainNumber}. Drop one of the two.`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
const nodes = await listNodes(options.origin, resolved.systemId, options.request, {
|
|
216
|
+
...(options.protocol === undefined ? {} : { protocol: options.protocol }),
|
|
217
|
+
...(options.port === undefined ? {} : { port: options.port }),
|
|
218
|
+
})
|
|
219
|
+
const usable = nodes.filter(isUsable)
|
|
220
|
+
|
|
221
|
+
if (usable.length === 0) {
|
|
222
|
+
const seen = nodes.map((n) => `${n.host} (${n.kind}/${n.status})`).join(', ')
|
|
223
|
+
throw nodeUnusable(
|
|
224
|
+
`The directory at ${options.origin} lists no node for chain ${resolved.chainNumber} ` +
|
|
225
|
+
'that is active and running. Only an active node processes transactions. ' +
|
|
226
|
+
(seen === '' ? 'It listed none at all.' : `It listed: ${seen}.`),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Object.freeze({ chain: resolved, nodes: Object.freeze([...nodes]), usable: Object.freeze(usable) })
|
|
231
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { GeeqError, invalidArgument, nodeUnusable } from '../errors'
|
|
2
|
+
import { fromBase64 } from '../encoding/base64'
|
|
3
|
+
import { toHex } from '../encoding/hex'
|
|
4
|
+
import { explorerTransactionRoute } from './routes'
|
|
5
|
+
import { defaultTransport, requestText } from './transport'
|
|
6
|
+
import type { GeeqTransport } from './transport'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 10_000
|
|
9
|
+
const HASH_BYTES = 32
|
|
10
|
+
/** What the explorer answers, with HTTP 500, for a refHash it does not have. */
|
|
11
|
+
const NOT_FOUND = 'Transaction does not exist'
|
|
12
|
+
|
|
13
|
+
/** A transaction as a block explorer reports it, field for field. */
|
|
14
|
+
export interface GeeqExplorerTransaction {
|
|
15
|
+
/** Hex — the form the explorer files transactions under. */
|
|
16
|
+
readonly refHash: string
|
|
17
|
+
/** The transaction's kind, such as `coin`. */
|
|
18
|
+
readonly type: string
|
|
19
|
+
/** The explorer's verdict on it. */
|
|
20
|
+
readonly status: string
|
|
21
|
+
readonly block: number
|
|
22
|
+
readonly chainName: string
|
|
23
|
+
readonly chainNumber: number
|
|
24
|
+
/** The sender's wallet address. */
|
|
25
|
+
readonly from: string
|
|
26
|
+
readonly senderType: string
|
|
27
|
+
/** The receiver's wallet address, or empty when it has none. */
|
|
28
|
+
readonly to: string
|
|
29
|
+
readonly receiverType: string
|
|
30
|
+
readonly publicKey: string
|
|
31
|
+
readonly signature: string
|
|
32
|
+
readonly createdAt: string
|
|
33
|
+
readonly fee: string
|
|
34
|
+
readonly acctNumApp?: string
|
|
35
|
+
/** What it moved — coin, tokens, NFTs — with amounts as decimal strings. */
|
|
36
|
+
readonly values: readonly {
|
|
37
|
+
readonly type: string
|
|
38
|
+
readonly value: string
|
|
39
|
+
readonly name?: string
|
|
40
|
+
}[]
|
|
41
|
+
/** An attestation's hash and metadata. */
|
|
42
|
+
readonly payload?: {
|
|
43
|
+
readonly attestationHash: string
|
|
44
|
+
readonly metadata: readonly string[]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ExplorerTransactionRequest {
|
|
49
|
+
/** The explorer's base URL, such as `https://blkexp.geeq-test.io`. */
|
|
50
|
+
readonly explorerUrl: string | URL
|
|
51
|
+
/**
|
|
52
|
+
* The transaction's refHash in base64 — what `transactionIdOf(tx)` returns and socket
|
|
53
|
+
* status events carry. It is sent to the explorer as hex.
|
|
54
|
+
*/
|
|
55
|
+
readonly refHash: string
|
|
56
|
+
/** Defaults to `globalThis.fetch`. */
|
|
57
|
+
readonly transport?: GeeqTransport
|
|
58
|
+
/** Defaults to 10000. */
|
|
59
|
+
readonly timeoutMs?: number
|
|
60
|
+
readonly signal?: AbortSignal
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function explorerBase(explorerUrl: string | URL): string {
|
|
64
|
+
let url: URL | undefined
|
|
65
|
+
try {
|
|
66
|
+
url = new URL(String(explorerUrl))
|
|
67
|
+
} catch {
|
|
68
|
+
url = undefined
|
|
69
|
+
}
|
|
70
|
+
if (url === undefined || (url.protocol !== 'https:' && url.protocol !== 'http:')) {
|
|
71
|
+
throw invalidArgument(
|
|
72
|
+
'explorerUrl must be an absolute http(s) URL, such as https://blkexp.geeq-test.io, ' +
|
|
73
|
+
`received ${JSON.stringify(String(explorerUrl))}.`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
return `${url.origin}${url.pathname.replace(/\/+$/u, '')}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Look a transaction up in a block explorer by its refHash.
|
|
81
|
+
*
|
|
82
|
+
* Resolves `null` when the explorer does not have it — not indexed yet, or never put in a
|
|
83
|
+
* block. The explorer only knows a transaction once a block has decided it, so for one
|
|
84
|
+
* you have just sent, the node socket answers first.
|
|
85
|
+
*/
|
|
86
|
+
export async function getExplorerTransaction(
|
|
87
|
+
request: ExplorerTransactionRequest,
|
|
88
|
+
): Promise<GeeqExplorerTransaction | null> {
|
|
89
|
+
const hash = fromBase64(request.refHash, 'refHash')
|
|
90
|
+
if (hash.length !== HASH_BYTES) {
|
|
91
|
+
throw invalidArgument(
|
|
92
|
+
`refHash must be the base64 of a ${HASH_BYTES}-byte hash, received ${hash.length} bytes.`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
const url = explorerTransactionRoute(explorerBase(request.explorerUrl), toHex(hash))
|
|
96
|
+
|
|
97
|
+
let body: string
|
|
98
|
+
try {
|
|
99
|
+
body = await requestText(url, {
|
|
100
|
+
transport: request.transport ?? defaultTransport,
|
|
101
|
+
timeoutMs: request.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
102
|
+
retries: 1,
|
|
103
|
+
signal: request.signal,
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof GeeqError && error.nodeDetails === NOT_FOUND) return null
|
|
107
|
+
throw error
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let result: unknown
|
|
111
|
+
try {
|
|
112
|
+
result = (JSON.parse(body) as { result?: unknown })?.result
|
|
113
|
+
} catch {
|
|
114
|
+
result = undefined
|
|
115
|
+
}
|
|
116
|
+
if (result === null || typeof result !== 'object' || !('refHash' in result)) {
|
|
117
|
+
throw nodeUnusable(
|
|
118
|
+
`${url} did not answer with a transaction, so it does not look like a Geeq block ` +
|
|
119
|
+
'explorer.',
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
return result as GeeqExplorerTransaction
|
|
123
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ledgerUnreadable } from '../errors'
|
|
2
|
+
import { decodeValidationLedger } from '../ledger/decode'
|
|
3
|
+
import type { GeeqLedger } from '../ledger/geeq-ledger'
|
|
4
|
+
import { decodeNftRecords } from '../ledger/nft-records'
|
|
5
|
+
import type { NftRecordFacts } from '../ledger/nft-records'
|
|
6
|
+
import { appLedgerRoute, ledgerRoute } from './routes'
|
|
7
|
+
import type { RequestOptions } from './transport'
|
|
8
|
+
import { requestText } from './transport'
|
|
9
|
+
|
|
10
|
+
/** Pull `result` out of a node's response envelope. */
|
|
11
|
+
function resultOf(body: string, url: string): string {
|
|
12
|
+
let parsed: unknown
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(body)
|
|
15
|
+
} catch (cause) {
|
|
16
|
+
throw ledgerUnreadable(
|
|
17
|
+
`The node at ${url} did not answer with JSON. A login page or a proxy error is ` +
|
|
18
|
+
`the usual reason. First 120 characters: ${JSON.stringify(body.slice(0, 120))}.`,
|
|
19
|
+
cause,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (parsed === null || typeof parsed !== 'object' || !('result' in parsed)) {
|
|
24
|
+
throw ledgerUnreadable(
|
|
25
|
+
`The node at ${url} answered JSON with no "result" field, so this is probably ` +
|
|
26
|
+
'not a Geeq node.',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { result } = parsed as { result: unknown }
|
|
31
|
+
if (typeof result !== 'string') {
|
|
32
|
+
throw ledgerUnreadable(
|
|
33
|
+
`The node at ${url} answered with a "result" of type ${typeof result}, expected a ` +
|
|
34
|
+
'base64 string.',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read and decode the validation layer ledger from one node. */
|
|
41
|
+
export async function readLedger(
|
|
42
|
+
origin: string,
|
|
43
|
+
options: RequestOptions,
|
|
44
|
+
): Promise<GeeqLedger> {
|
|
45
|
+
const url = ledgerRoute(origin)
|
|
46
|
+
const body = await requestText(url, options)
|
|
47
|
+
return decodeValidationLedger(resultOf(body, url), { nodeUrl: origin })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read the NFT records in the application layer ledger from one node. */
|
|
51
|
+
export async function readNftRecords(
|
|
52
|
+
origin: string,
|
|
53
|
+
options: RequestOptions,
|
|
54
|
+
): Promise<Map<string, NftRecordFacts>> {
|
|
55
|
+
const url = appLedgerRoute(origin)
|
|
56
|
+
const body = await requestText(url, options)
|
|
57
|
+
return decodeNftRecords(resultOf(body, url))
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every node route the SDK knows, in one place.
|
|
3
|
+
*
|
|
4
|
+
* `origin` is an absolute node URL with no trailing slash — see `originOf`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** `GET` the current validation layer ledger, base64 packed capnp under `result`. */
|
|
8
|
+
export function ledgerRoute(origin: string): string {
|
|
9
|
+
return `${origin}/api/blocks?type=vll`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `GET` the current application layer ledger, where NFT records live. Same envelope. */
|
|
13
|
+
export function appLedgerRoute(origin: string): string {
|
|
14
|
+
return `${origin}/api/blocks?type=all`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Where a signed transaction is POSTed.
|
|
19
|
+
*
|
|
20
|
+
* The SDK does not submit — this is exported so an application builds the same URL the
|
|
21
|
+
* SDK read from, rather than keeping its own copy of the path.
|
|
22
|
+
*/
|
|
23
|
+
export function submitRoute(origin: string): string {
|
|
24
|
+
return `${origin}/api/blocks/process`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The node's client WebSocket, where transaction status events arrive.
|
|
29
|
+
*
|
|
30
|
+
* Same host and port as the HTTP API — the node's TLS front door routes `/ws/` to the
|
|
31
|
+
* socket server and `/api/` to the HTTP one — so the only change is the scheme. The
|
|
32
|
+
* trailing slash is part of the route: the proxy answers `/ws` with a redirect, and a
|
|
33
|
+
* WebSocket handshake treats a redirect as failure.
|
|
34
|
+
*/
|
|
35
|
+
export function socketRoute(origin: string): string {
|
|
36
|
+
const url = new URL(origin)
|
|
37
|
+
const scheme =
|
|
38
|
+
url.protocol === 'https:' ? 'wss:' : url.protocol === 'http:' ? 'ws:' : url.protocol
|
|
39
|
+
return `${scheme}//${url.host}${url.pathname.replace(/\/+$/u, '')}/ws/`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The body a node expects for an application-layer transaction: an attestation or an NFT
|
|
44
|
+
* mint, each a transaction and its payload.
|
|
45
|
+
*/
|
|
46
|
+
export function attestationSubmitBody(transaction: string, payload: string): string {
|
|
47
|
+
return JSON.stringify({ type: 'uat', data: { transaction, payload } })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The body a node expects for a coin transfer.
|
|
52
|
+
*
|
|
53
|
+
* A different type and a different shape from an attestation: a validation-layer
|
|
54
|
+
* transaction carries no separate payload, so there is one blob rather than two.
|
|
55
|
+
*/
|
|
56
|
+
export function coinTransferSubmitBody(transaction: string): string {
|
|
57
|
+
return JSON.stringify({ type: 'uvt', data: { transaction } })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Discovery routes on the chain's directory service.
|
|
62
|
+
*
|
|
63
|
+
* Only these two are ever called. The same service also exposes routes that start and
|
|
64
|
+
* stop nodes, install a genesis block, and — on `/api/accounts` — hand out plaintext
|
|
65
|
+
* private keys. None of those belong anywhere near an SDK, so the paths live here and
|
|
66
|
+
* nowhere else, and nothing constructs a URL against that origin by hand.
|
|
67
|
+
*/
|
|
68
|
+
export function chainsRoute(origin: string): string {
|
|
69
|
+
return `${origin}/api/admin/simulation/chains`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function nodesRoute(origin: string, systemId: number): string {
|
|
73
|
+
return `${origin}/api/nodes?systemId=${encodeURIComponent(String(systemId))}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A block explorer's transaction lookup. The explorer files transactions by hex refHash. */
|
|
77
|
+
export function explorerTransactionRoute(explorer: string, refHashHex: string): string {
|
|
78
|
+
return `${explorer}/api/transactions/byRefHash?refHash=${refHashHex}`
|
|
79
|
+
}
|