@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,149 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #f6f7f9;
|
|
3
|
+
--surface: #ffffff;
|
|
4
|
+
--surface-2: #f2f4f7;
|
|
5
|
+
--border: #e3e6ea;
|
|
6
|
+
--text: #16191d;
|
|
7
|
+
--muted: #6b7280;
|
|
8
|
+
--accent: #2f6df6;
|
|
9
|
+
--accent-text: #ffffff;
|
|
10
|
+
--danger: #b42318;
|
|
11
|
+
--danger-bg: #fef3f2;
|
|
12
|
+
--danger-border: #fecdca;
|
|
13
|
+
--ok: #067647;
|
|
14
|
+
--ok-bg: #ecfdf3;
|
|
15
|
+
--radius: 10px;
|
|
16
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (prefers-color-scheme: dark) {
|
|
20
|
+
:root {
|
|
21
|
+
--bg: #0e1116;
|
|
22
|
+
--surface: #161b22;
|
|
23
|
+
--surface-2: #1c222b;
|
|
24
|
+
--border: #2a313c;
|
|
25
|
+
--text: #e6edf3;
|
|
26
|
+
--muted: #9198a1;
|
|
27
|
+
--accent: #4d84ff;
|
|
28
|
+
--danger: #ff8080;
|
|
29
|
+
--danger-bg: #2a1616;
|
|
30
|
+
--danger-border: #5c2626;
|
|
31
|
+
--ok: #4ade80;
|
|
32
|
+
--ok-bg: #12251a;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
|
|
38
|
+
body {
|
|
39
|
+
margin: 0;
|
|
40
|
+
background: var(--bg);
|
|
41
|
+
color: var(--text);
|
|
42
|
+
font: 15px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
43
|
+
-webkit-font-smoothing: antialiased;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.wrap { max-width: 780px; margin: 0 auto; padding: 40px 20px 80px; }
|
|
47
|
+
|
|
48
|
+
header h1 { font-size: 24px; margin: 0 0 6px; letter-spacing: -0.02em; }
|
|
49
|
+
header p { margin: 0; color: var(--muted); }
|
|
50
|
+
|
|
51
|
+
.engine {
|
|
52
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
53
|
+
margin-top: 14px; padding: 5px 11px;
|
|
54
|
+
border: 1px solid var(--border); border-radius: 999px;
|
|
55
|
+
background: var(--surface); font-size: 13px; color: var(--muted);
|
|
56
|
+
}
|
|
57
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
|
|
58
|
+
.dot.loading { background: #f59e0b; animation: pulse 1.1s ease-in-out infinite; }
|
|
59
|
+
.dot.ready { background: var(--ok); }
|
|
60
|
+
.dot.error { background: var(--danger); }
|
|
61
|
+
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.35 } }
|
|
62
|
+
|
|
63
|
+
.card {
|
|
64
|
+
background: var(--surface);
|
|
65
|
+
border: 1px solid var(--border);
|
|
66
|
+
border-radius: var(--radius);
|
|
67
|
+
padding: 20px;
|
|
68
|
+
margin-top: 20px;
|
|
69
|
+
}
|
|
70
|
+
.card h2 {
|
|
71
|
+
font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
72
|
+
color: var(--muted); margin: 0 0 16px; font-weight: 600;
|
|
73
|
+
}
|
|
74
|
+
.card h2 .step {
|
|
75
|
+
display: inline-block; width: 19px; height: 19px; line-height: 19px;
|
|
76
|
+
text-align: center; border-radius: 50%;
|
|
77
|
+
background: var(--surface-2); border: 1px solid var(--border);
|
|
78
|
+
color: var(--text); margin-right: 8px; font-size: 11px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* A step waiting on an earlier one: the note sits on top of the card like a tab, and the
|
|
82
|
+
card stays readable but dimmed. The heading keeps full strength so the order reads. */
|
|
83
|
+
.gate-note {
|
|
84
|
+
margin: 20px 0 0; padding: 8px 14px;
|
|
85
|
+
border: 1px solid var(--border); border-bottom: 0;
|
|
86
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
87
|
+
background: var(--surface-2); color: var(--muted); font-size: 13px;
|
|
88
|
+
}
|
|
89
|
+
.gate.locked .card { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
90
|
+
.gate.locked .card > :not(h2) { opacity: 0.45; }
|
|
91
|
+
.gate.done .card h2 .step { background: var(--ok-bg); border-color: var(--ok); color: var(--ok); }
|
|
92
|
+
|
|
93
|
+
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
94
|
+
.row + .row { margin-top: 12px; }
|
|
95
|
+
.grow { flex: 1 1 auto; }
|
|
96
|
+
|
|
97
|
+
button {
|
|
98
|
+
font: inherit; font-weight: 500;
|
|
99
|
+
padding: 9px 15px; border-radius: 8px;
|
|
100
|
+
border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
|
|
101
|
+
cursor: pointer; transition: filter .12s ease;
|
|
102
|
+
}
|
|
103
|
+
button:hover:not(:disabled) { filter: brightness(0.97); }
|
|
104
|
+
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
105
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
106
|
+
button.small { padding: 5px 10px; font-size: 13px; }
|
|
107
|
+
|
|
108
|
+
select, input[type="number"], textarea {
|
|
109
|
+
font: inherit; color: var(--text);
|
|
110
|
+
background: var(--surface-2); border: 1px solid var(--border);
|
|
111
|
+
border-radius: 8px; padding: 9px 11px;
|
|
112
|
+
}
|
|
113
|
+
textarea { width: 100%; font-family: var(--mono); font-size: 13.5px; resize: vertical; min-height: 78px; }
|
|
114
|
+
label { font-size: 13px; color: var(--muted); }
|
|
115
|
+
|
|
116
|
+
.phrase {
|
|
117
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
118
|
+
gap: 8px; margin: 4px 0 14px;
|
|
119
|
+
}
|
|
120
|
+
.word {
|
|
121
|
+
display: flex; align-items: baseline; gap: 7px;
|
|
122
|
+
background: var(--surface-2); border: 1px solid var(--border);
|
|
123
|
+
border-radius: 7px; padding: 8px 11px; font-family: var(--mono); font-size: 13.5px;
|
|
124
|
+
}
|
|
125
|
+
.word .n { color: var(--muted); font-size: 11px; min-width: 15px; }
|
|
126
|
+
|
|
127
|
+
.field { margin-top: 13px; }
|
|
128
|
+
.field .k { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
129
|
+
.field .v {
|
|
130
|
+
font-family: var(--mono); font-size: 13px; word-break: break-all;
|
|
131
|
+
background: var(--surface-2); border: 1px solid var(--border);
|
|
132
|
+
border-radius: 7px; padding: 9px 11px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.warn {
|
|
136
|
+
background: var(--danger-bg); border: 1px solid var(--danger-border);
|
|
137
|
+
color: var(--danger); border-radius: 8px; padding: 11px 13px;
|
|
138
|
+
font-size: 13.5px; margin-top: 14px;
|
|
139
|
+
}
|
|
140
|
+
.warn strong { font-weight: 600; }
|
|
141
|
+
|
|
142
|
+
.hint { font-size: 13px; color: var(--muted); margin-top: 10px; }
|
|
143
|
+
.valid { color: var(--ok); }
|
|
144
|
+
.invalid { color: var(--danger); }
|
|
145
|
+
.empty { color: var(--muted); font-size: 14px; padding: 6px 0; }
|
|
146
|
+
|
|
147
|
+
footer { margin-top: 30px; font-size: 13px; color: var(--muted); text-align: center; }
|
|
148
|
+
code { font-family: var(--mono); font-size: 0.92em; background: var(--surface-2);
|
|
149
|
+
padding: 1px 5px; border-radius: 4px; border: 1px solid var(--border); }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUnusedLocals": true,
|
|
9
|
+
"noUnusedParameters": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"types": ["vite/client"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
import { defineConfig } from 'vite'
|
|
4
|
+
import vue from '@vitejs/plugin-vue'
|
|
5
|
+
|
|
6
|
+
// `@geeqdev/geeq-sdk` is a `file:` dependency, so node_modules holds a symlink and
|
|
7
|
+
// Vite serves it from its real path under `/@fs/`. The engine lives next to the
|
|
8
|
+
// bundle there, outside this project's root, so dev needs explicit read access —
|
|
9
|
+
// without it the .wasm request 403s and initGeeq() rejects with `init-failed`.
|
|
10
|
+
const require = createRequire(import.meta.url)
|
|
11
|
+
const sdkRoot = dirname(require.resolve('@geeqdev/geeq-sdk/package.json'))
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [vue()],
|
|
15
|
+
optimizeDeps: {
|
|
16
|
+
// Required by @geeqdev/geeq-sdk. Vite's dependency pre-bundling rewrites the
|
|
17
|
+
// `new URL('...wasm', import.meta.url)` the SDK uses to locate its engine;
|
|
18
|
+
// excluding it keeps that pattern intact so the .wasm is emitted as an asset.
|
|
19
|
+
exclude: ['@geeqdev/geeq-sdk'],
|
|
20
|
+
},
|
|
21
|
+
server: {
|
|
22
|
+
fs: {
|
|
23
|
+
allow: ['.', sdkRoot],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
moduleNameMapper: {
|
|
6
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
7
|
+
// Jest runs as CommonJS. Swap the ESM browser engine for the vendored
|
|
8
|
+
// CommonJS build, which instantiates itself on import, and swap the shipped
|
|
9
|
+
// loaders for a no-op. scripts/smoke.mjs covers the real ones.
|
|
10
|
+
'^.*/wasm/browser/geeq_core$': '<rootDir>/src/wasm/nodejs/geeq_core',
|
|
11
|
+
'^.*/wasm-loader\\.browser$': '<rootDir>/tests/wasm-loader.jest.ts',
|
|
12
|
+
},
|
|
13
|
+
transform: {
|
|
14
|
+
'^.+\\.tsx?$': ['ts-jest', { tsconfig: { module: 'CommonJS', verbatimModuleSyntax: false } }],
|
|
15
|
+
},
|
|
16
|
+
testMatch: ['<rootDir>/tests/**/*.test.ts'],
|
|
17
|
+
collectCoverageFrom: ['src/**/*.ts'],
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geeqdev/geeq-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Geeq blockchain SDK for wallets, transfers and NFTs. Works in the browser and Node.js.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"geeq",
|
|
@@ -40,10 +40,6 @@
|
|
|
40
40
|
"./geeq_core_bg.wasm": "./dist/browser/geeq_core_bg.wasm",
|
|
41
41
|
"./package.json": "./package.json"
|
|
42
42
|
},
|
|
43
|
-
"files": [
|
|
44
|
-
"dist",
|
|
45
|
-
"README.md"
|
|
46
|
-
],
|
|
47
43
|
"publishConfig": {
|
|
48
44
|
"registry": "https://registry.npmjs.org/",
|
|
49
45
|
"access": "public"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copies the engine next to each build output, so the `new URL('./geeq_core_bg.wasm',
|
|
2
|
+
// import.meta.url)` in our own source resolves for both targets.
|
|
3
|
+
import { copyFile, mkdir, stat } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
8
|
+
|
|
9
|
+
const source = join(root, 'src', 'wasm', 'browser', 'geeq_core_bg.wasm')
|
|
10
|
+
|
|
11
|
+
const info = await stat(source)
|
|
12
|
+
if (info.size === 0) {
|
|
13
|
+
throw new Error(`Refusing to ship an empty engine: ${source}`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const target of ['dist/browser', 'dist/node']) {
|
|
17
|
+
const dir = join(root, target)
|
|
18
|
+
await mkdir(dir, { recursive: true })
|
|
19
|
+
await copyFile(source, join(dir, 'geeq_core_bg.wasm'))
|
|
20
|
+
console.log(`copied geeq_core_bg.wasm -> ${target} (${(info.size / 1024 / 1024).toFixed(2)} MB)`)
|
|
21
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaging proof. Runs the real built bundles — not src/ — so it catches the
|
|
3
|
+
* failures unit tests structurally cannot: a broken exports map, a WASM the
|
|
4
|
+
* loader cannot find, or an import.meta.url that did not survive the build.
|
|
5
|
+
*
|
|
6
|
+
* node scripts/smoke.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto'
|
|
9
|
+
import { createServer } from 'node:http'
|
|
10
|
+
import { createRequire } from 'node:module'
|
|
11
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import assert from 'node:assert/strict'
|
|
15
|
+
|
|
16
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
17
|
+
const require = createRequire(import.meta.url)
|
|
18
|
+
let checks = 0
|
|
19
|
+
const ok = (label) => {
|
|
20
|
+
checks += 1
|
|
21
|
+
console.log(` ok ${label}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The vendored Cap'n Proto schemas, compiled from src/capnp for this script. A plain
|
|
26
|
+
* .mjs cannot import TypeScript, so esbuild (tsup's engine) builds the ones the ledger
|
|
27
|
+
* needs, leaving capnp-ts external so they share the `Message` this script uses.
|
|
28
|
+
*/
|
|
29
|
+
async function loadSchemas() {
|
|
30
|
+
const { build } = await import('esbuild')
|
|
31
|
+
const { outputFiles } = await build({
|
|
32
|
+
stdin: {
|
|
33
|
+
contents: [
|
|
34
|
+
"export { ValLayLedger } from './valLayLedger.capnp'",
|
|
35
|
+
"export { NetActorStatus, NetActorType } from './netActorRec.capnp'",
|
|
36
|
+
].join('\n'),
|
|
37
|
+
resolveDir: join(root, 'src/capnp'),
|
|
38
|
+
loader: 'ts',
|
|
39
|
+
},
|
|
40
|
+
bundle: true,
|
|
41
|
+
write: false,
|
|
42
|
+
format: 'cjs',
|
|
43
|
+
platform: 'node',
|
|
44
|
+
external: ['capnp-ts'],
|
|
45
|
+
logLevel: 'silent',
|
|
46
|
+
})
|
|
47
|
+
const module = { exports: {} }
|
|
48
|
+
new Function('require', 'module', 'exports', outputFiles[0].text)(require, module, module.exports)
|
|
49
|
+
return module.exports
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A minimal validation layer ledger, built with the same Cap'n Proto schema the SDK
|
|
54
|
+
* bundles and decodes with — the check that the decoder survived the build, which no
|
|
55
|
+
* unit test can prove, because unit tests import source rather than the built package.
|
|
56
|
+
*/
|
|
57
|
+
async function buildLedger(accountId) {
|
|
58
|
+
const { Message } = require('capnp-ts')
|
|
59
|
+
const { ValLayLedger, NetActorStatus, NetActorType } = await loadSchemas()
|
|
60
|
+
|
|
61
|
+
const message = new Message()
|
|
62
|
+
const vll = message.initRoot(ValLayLedger)
|
|
63
|
+
vll.setChainNum(1)
|
|
64
|
+
vll.setBlockNum(41)
|
|
65
|
+
|
|
66
|
+
const actors = vll.initNetActorList().initNetActorRecs(1)
|
|
67
|
+
const actor = actors.get(0)
|
|
68
|
+
actor.setIpAddr('10.0.0.1')
|
|
69
|
+
actor.setPortNum(2082)
|
|
70
|
+
actor.setNodeNumSelf(3)
|
|
71
|
+
// A real ledger sets this 100-355 blocks ahead; the capnp zero would read as a node
|
|
72
|
+
// already rotating out of the active set.
|
|
73
|
+
actor.setBlockNumRemove(241)
|
|
74
|
+
actor.setNetActorType(NetActorType.NET_ACTOR_TYPE_ANON_NODE)
|
|
75
|
+
actor.setRecStatus(NetActorStatus.NET_ACTOR_STATUS_NODE)
|
|
76
|
+
actor.initAcctNumVal(32)
|
|
77
|
+
actor.getAcctNumVal().copyBuffer(new Uint8Array(32).fill(0xa0))
|
|
78
|
+
|
|
79
|
+
const params = vll.initParamAcctList().initParamAcctRecs(2)
|
|
80
|
+
const num = params.get(0).initParamType().initMetadataVarNum()
|
|
81
|
+
num.setCurrentValue(4)
|
|
82
|
+
const bytes = params.get(1).initParamType().initMetadataVarBytes()
|
|
83
|
+
bytes.setCurrentValue(64)
|
|
84
|
+
|
|
85
|
+
const accounts = vll.initCoinAcctRecs(1)
|
|
86
|
+
const account = accounts.get(0)
|
|
87
|
+
account.initAcctNumVal(32)
|
|
88
|
+
account.getAcctNumVal().copyBuffer(accountId)
|
|
89
|
+
account.getRecType().initAsset().setNonce(7)
|
|
90
|
+
|
|
91
|
+
return Buffer.from(message.toPackedArrayBuffer()).toString('base64')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function exercise(sdk, label) {
|
|
95
|
+
console.log(`\n${label}`)
|
|
96
|
+
|
|
97
|
+
const keypair = await sdk.generateKeypair()
|
|
98
|
+
assert.equal(keypair.publicKey.length, 32, 'public key is 32 bytes')
|
|
99
|
+
assert.equal(keypair.privateKey.length, 32, 'private key is 32 bytes')
|
|
100
|
+
assert.equal(keypair.accountId.length, 32, 'account id is 32 bytes')
|
|
101
|
+
ok('generateKeypair')
|
|
102
|
+
|
|
103
|
+
const restored = await sdk.importKeypair(keypair.publicKey, keypair.privateKey)
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
Buffer.from(restored.accountId),
|
|
106
|
+
Buffer.from(keypair.accountId),
|
|
107
|
+
'imported keypair has the same account id',
|
|
108
|
+
)
|
|
109
|
+
ok('importKeypair round-trip')
|
|
110
|
+
|
|
111
|
+
const address = keypair.getAddress({ chainNumber: 1 })
|
|
112
|
+
assert.ok(typeof address === 'string' && address.length > 0, 'address is a non-empty string')
|
|
113
|
+
ok(`createWalletAddress -> ${address.slice(0, 16)}...`)
|
|
114
|
+
|
|
115
|
+
const decoded = await sdk.decodeWalletAddress(address)
|
|
116
|
+
assert.equal(decoded.chainNumber, 1)
|
|
117
|
+
assert.equal(decoded.recordType, 'coin-asset')
|
|
118
|
+
assert.deepEqual(Buffer.from(decoded.accountId), Buffer.from(keypair.accountId))
|
|
119
|
+
ok('decodeWalletAddress round-trip')
|
|
120
|
+
|
|
121
|
+
const message = new TextEncoder().encode('geeq packaging smoke test')
|
|
122
|
+
const signature = keypair.sign(message)
|
|
123
|
+
assert.equal(signature.length, 64)
|
|
124
|
+
assert.equal(sdk.verifySignature(keypair.publicKey, signature, message), true)
|
|
125
|
+
ok('sign / verify')
|
|
126
|
+
|
|
127
|
+
const mnemonic = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
|
|
128
|
+
const a = await sdk.keypairFromMnemonic(mnemonic)
|
|
129
|
+
const b = await sdk.keypairFromMnemonic(mnemonic)
|
|
130
|
+
assert.deepEqual(Buffer.from(a.publicKey), Buffer.from(b.publicKey), 'mnemonic is deterministic')
|
|
131
|
+
ok('keypairFromMnemonic is deterministic')
|
|
132
|
+
|
|
133
|
+
// Chain state read through a scripted transport — no node, no network.
|
|
134
|
+
const ledger = await buildLedger(a.accountId)
|
|
135
|
+
// Counts calls so the smoke can assert that connecting is genuinely free.
|
|
136
|
+
const transport = Object.assign(
|
|
137
|
+
async (url) => {
|
|
138
|
+
transport.calls += 1
|
|
139
|
+
if (url.includes('/admin/simulation/chains')) {
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
status: 200,
|
|
143
|
+
text: async () => JSON.stringify({ result: [{ id: 9, chainNumber: 1, name: 'smoke' }] }),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (url.includes('/api/nodes')) {
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
status: 200,
|
|
150
|
+
text: async () =>
|
|
151
|
+
JSON.stringify({ result: [{ ip: '10.0.0.1', type: 'active', status: 'ready' }] }),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, status: 200, text: async () => JSON.stringify({ result: ledger }) }
|
|
155
|
+
},
|
|
156
|
+
{ calls: 0 },
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const chain = await sdk.GeeqChain.connect({ penniesPerGeeq: 1_000_000_000n, endpoint: 'https://10.0.0.1:2082', transport })
|
|
160
|
+
assert.equal(transport.calls, 0, 'connecting reads nothing from the chain')
|
|
161
|
+
assert.equal(chain.node, undefined, 'the node number is not known before a ledger read')
|
|
162
|
+
ok('GeeqChain.connect performs no I/O')
|
|
163
|
+
|
|
164
|
+
const state = await chain.getChainState({ account: a })
|
|
165
|
+
assert.equal(chain.chainNumber, 1, 'chain number comes from the ledger')
|
|
166
|
+
assert.equal(chain.node.nodeNumber, 3, 'node number is nodeNumSelf for the matched host')
|
|
167
|
+
ok(`first ledger read -> chain ${chain.chainNumber}, node ${chain.node.nodeNumber}`)
|
|
168
|
+
|
|
169
|
+
assert.equal(state.blockNumber, 42, 'target block is the ledger block plus one')
|
|
170
|
+
assert.equal(state.nonce, 7, 'nonce comes from the coin-asset record')
|
|
171
|
+
ok('getChainState derives block and nonce')
|
|
172
|
+
|
|
173
|
+
const attestation = await chain.createAttestation({ keypair: a, content: Buffer.from('smoke') })
|
|
174
|
+
assert.ok(attestation.transaction.length > 0, 'attestation is built')
|
|
175
|
+
assert.equal(attestation.nodeUrl, 'https://10.0.0.1:2082', 'it knows where to be submitted')
|
|
176
|
+
assert.equal(chain.node.submitUrl, 'https://10.0.0.1:2082/api/blocks/process', 'submit route')
|
|
177
|
+
ok('createAttestation through the built bundle')
|
|
178
|
+
chain.dispose()
|
|
179
|
+
|
|
180
|
+
// Discovery: the directory names an active node, and no ledger is read to do it.
|
|
181
|
+
transport.calls = 0
|
|
182
|
+
const discovered = await sdk.GeeqChain.connect({
|
|
183
|
+
penniesPerGeeq: 1_000_000_000n,
|
|
184
|
+
discovery: 'https://simulator.example.com',
|
|
185
|
+
chainNumber: 1,
|
|
186
|
+
transport,
|
|
187
|
+
})
|
|
188
|
+
assert.equal(discovered.endpoint, 'https://10.0.0.1:2082', 'picked the active node')
|
|
189
|
+
assert.equal(transport.calls, 2, 'discovery reads exactly two directory routes')
|
|
190
|
+
ok('GeeqChain.connect via the node directory')
|
|
191
|
+
discovered.dispose()
|
|
192
|
+
|
|
193
|
+
// The explorer is asked in hex for a refHash given in base64.
|
|
194
|
+
const refHash = new Uint8Array(32).fill(0xab)
|
|
195
|
+
let asked = ''
|
|
196
|
+
const found = await sdk.getExplorerTransaction({
|
|
197
|
+
explorerUrl: 'https://blkexp.example.com',
|
|
198
|
+
refHash: sdk.toBase64(refHash),
|
|
199
|
+
transport: async (url) => {
|
|
200
|
+
asked = url
|
|
201
|
+
return { ok: true, status: 200, text: async () => JSON.stringify({ result: { refHash: sdk.toHex(refHash), status: 'valid' } }) }
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
assert.ok(asked.endsWith(`/api/transactions/byRefHash?refHash=${sdk.toHex(refHash)}`), 'asks in hex')
|
|
205
|
+
assert.equal(found.status, 'valid')
|
|
206
|
+
ok('getExplorerTransaction asks the explorer in hex')
|
|
207
|
+
|
|
208
|
+
for (const kp of [keypair, restored, a, b]) kp.dispose()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The smallest WebSocket server that speaks enough RFC 6455 to be a node's socket: text
|
|
213
|
+
* frames both ways, a close, and the `/ws/` path. Node has a WebSocket client but no
|
|
214
|
+
* server, and the point is a real handshake with the runtime's own client — no fakes.
|
|
215
|
+
*/
|
|
216
|
+
async function socketServer() {
|
|
217
|
+
const peers = []
|
|
218
|
+
const server = createServer()
|
|
219
|
+
server.on('upgrade', (request, wire) => {
|
|
220
|
+
if (request.url !== '/ws/') {
|
|
221
|
+
wire.end('HTTP/1.1 404 Not Found\r\n\r\n')
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
const accept = createHash('sha1')
|
|
225
|
+
.update(`${request.headers['sec-websocket-key']}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
226
|
+
.digest('base64')
|
|
227
|
+
wire.write(
|
|
228
|
+
'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n' +
|
|
229
|
+
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const peer = {
|
|
233
|
+
frames: [],
|
|
234
|
+
waiters: [],
|
|
235
|
+
send(text) {
|
|
236
|
+
const body = Buffer.from(text)
|
|
237
|
+
const head =
|
|
238
|
+
body.length < 126
|
|
239
|
+
? Buffer.from([0x81, body.length])
|
|
240
|
+
: Buffer.from([0x81, 126, body.length >> 8, body.length & 0xff])
|
|
241
|
+
wire.write(Buffer.concat([head, body]))
|
|
242
|
+
},
|
|
243
|
+
/** Resolves with the next text frame the client sends. */
|
|
244
|
+
next() {
|
|
245
|
+
return new Promise((resolve) => peer.waiters.push(resolve))
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
peers.push(peer)
|
|
249
|
+
|
|
250
|
+
let pending = Buffer.alloc(0)
|
|
251
|
+
wire.on('data', (chunk) => {
|
|
252
|
+
pending = Buffer.concat([pending, chunk])
|
|
253
|
+
for (;;) {
|
|
254
|
+
if (pending.length < 2) return
|
|
255
|
+
const opcode = pending[0] & 0x0f
|
|
256
|
+
let length = pending[1] & 0x7f
|
|
257
|
+
let offset = 2
|
|
258
|
+
if (length === 126) {
|
|
259
|
+
length = pending.readUInt16BE(2)
|
|
260
|
+
offset = 4
|
|
261
|
+
}
|
|
262
|
+
if (pending.length < offset + 4 + length) return
|
|
263
|
+
const mask = pending.subarray(offset, offset + 4)
|
|
264
|
+
const body = Buffer.from(pending.subarray(offset + 4, offset + 4 + length))
|
|
265
|
+
for (let i = 0; i < body.length; i += 1) body[i] ^= mask[i % 4]
|
|
266
|
+
pending = pending.subarray(offset + 4 + length)
|
|
267
|
+
|
|
268
|
+
if (opcode === 0x8) {
|
|
269
|
+
wire.end(Buffer.from([0x88, 0]))
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
if (opcode !== 0x1) continue
|
|
273
|
+
const text = body.toString('utf8')
|
|
274
|
+
const waiter = peer.waiters.shift()
|
|
275
|
+
if (waiter === undefined) peer.frames.push(text)
|
|
276
|
+
else waiter(text)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
wire.on('error', () => {})
|
|
280
|
+
})
|
|
281
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
282
|
+
return { server, peers, port: server.address().port }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function exerciseSocket(sdk, label) {
|
|
286
|
+
console.log(`\n${label} socket`)
|
|
287
|
+
if (typeof globalThis.WebSocket !== 'function') {
|
|
288
|
+
console.log(' -- skipped: this Node has no global WebSocket (Node 22 does)')
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
assert.equal(sdk.socketRoute('https://10.0.0.1:2082'), 'wss://10.0.0.1:2082/ws/')
|
|
293
|
+
ok('socketRoute derives wss://…/ws/ from the node origin')
|
|
294
|
+
|
|
295
|
+
const { server, peers, port } = await socketServer()
|
|
296
|
+
const socket = await sdk.GeeqSocket.connect({ url: `ws://127.0.0.1:${port}/ws/` })
|
|
297
|
+
assert.equal(socket.state, 'open')
|
|
298
|
+
ok("GeeqSocket.connect handshakes with the runtime's own WebSocket")
|
|
299
|
+
|
|
300
|
+
const sender = await sdk.keypairFromMnemonic(
|
|
301
|
+
'legal winner thank year wave sausage worth useful legal winner thank yellow',
|
|
302
|
+
)
|
|
303
|
+
const receiver = await sdk.generateKeypair()
|
|
304
|
+
const tx = await sdk.createCoinTransferTx({
|
|
305
|
+
keypair: sender,
|
|
306
|
+
to: receiver.accountId,
|
|
307
|
+
amount: 150n,
|
|
308
|
+
chainState: { chainNumber: 1, blockNumber: 42, nodeNumber: 3, nonce: 7 },
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
const peer = peers[0]
|
|
312
|
+
const subscribed = peer.next()
|
|
313
|
+
const tracked = await socket.track(tx)
|
|
314
|
+
const frame = JSON.parse(await subscribed)
|
|
315
|
+
assert.equal(frame.event, 'client')
|
|
316
|
+
assert.ok(frame.payload.publicKeys.includes(sdk.toBase64(sender.publicKey)), 'signer key')
|
|
317
|
+
assert.ok(frame.payload.publicKeys.includes(sdk.toBase64(sender.accountId)), 'signer id')
|
|
318
|
+
ok('track() subscribes the signer before it resolves')
|
|
319
|
+
|
|
320
|
+
peer.send('pong')
|
|
321
|
+
peer.send(
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
event: 'uut_status',
|
|
324
|
+
payload: {
|
|
325
|
+
refHash: sdk.transactionIdOf(tx),
|
|
326
|
+
publicKeys: [sdk.toBase64(sender.publicKey)],
|
|
327
|
+
status: 'valid',
|
|
328
|
+
isValid: true,
|
|
329
|
+
blockNumber: 42,
|
|
330
|
+
vlbBlock: 43,
|
|
331
|
+
error: '',
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
)
|
|
335
|
+
const outcome = await tracked.settled()
|
|
336
|
+
assert.equal(outcome.status, 'confirmed', 'a valid status confirms the tracked transfer')
|
|
337
|
+
ok('a uut_status for the transfer hash settles it confirmed')
|
|
338
|
+
|
|
339
|
+
socket.dispose()
|
|
340
|
+
sender.dispose()
|
|
341
|
+
receiver.dispose()
|
|
342
|
+
server.closeAllConnections()
|
|
343
|
+
await new Promise((resolve) => server.close(resolve))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 1. Node ESM build — resolves the engine through import.meta.url.
|
|
347
|
+
const esm = await import(join(root, 'dist/node/index.js'))
|
|
348
|
+
await exercise(esm, 'dist/node/index.js (ESM)')
|
|
349
|
+
await exerciseSocket(esm, 'dist/node/index.js (ESM)')
|
|
350
|
+
|
|
351
|
+
// 2. Node CJS build — esbuild shims import.meta.url; this proves the shim works.
|
|
352
|
+
const cjs = require(join(root, 'dist/node/index.cjs'))
|
|
353
|
+
await exercise(cjs, 'dist/node/index.cjs (CJS)')
|
|
354
|
+
await exerciseSocket(cjs, 'dist/node/index.cjs (CJS)')
|
|
355
|
+
|
|
356
|
+
// 3. Browser bundle — cannot be executed here, so assert the contract a bundler relies on.
|
|
357
|
+
console.log('\ndist/browser/index.js (static checks)')
|
|
358
|
+
const browser = await readFile(join(root, 'dist/browser/index.js'), 'utf8')
|
|
359
|
+
assert.ok(
|
|
360
|
+
browser.includes("new URL(\"./geeq_core_bg.wasm\", import.meta.url)"),
|
|
361
|
+
'browser bundle keeps the new URL(..., import.meta.url) pattern bundlers key on',
|
|
362
|
+
)
|
|
363
|
+
ok('engine URL pattern preserved for bundlers')
|
|
364
|
+
assert.ok(!browser.includes('node:fs'), 'browser bundle pulls in no Node built-ins')
|
|
365
|
+
ok('no Node built-ins in the browser bundle')
|
|
366
|
+
|
|
367
|
+
for (const target of ['browser', 'node']) {
|
|
368
|
+
const wasm = await stat(join(root, `dist/${target}/geeq_core_bg.wasm`))
|
|
369
|
+
assert.ok(wasm.size > 1_000_000, `dist/${target} ships the engine`)
|
|
370
|
+
ok(`dist/${target}/geeq_core_bg.wasm (${(wasm.size / 1024 / 1024).toFixed(2)} MB)`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 4. The exports map must actually resolve for a consumer.
|
|
374
|
+
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'))
|
|
375
|
+
for (const [condition, targets] of Object.entries(pkg.exports['.'])) {
|
|
376
|
+
for (const [kind, rel] of Object.entries(targets)) {
|
|
377
|
+
await stat(join(root, rel))
|
|
378
|
+
ok(`exports["."].${condition}.${kind} -> ${rel}`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`\nAll ${checks} checks passed.`)
|