@zga/client 0.0.0-stage → 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/LICENSE +21 -0
- package/dist/addresses.d.ts +29 -0
- package/dist/addresses.js +43 -0
- package/dist/billing.d.ts +41 -0
- package/dist/billing.js +141 -0
- package/dist/bin/crank.d.ts +2 -0
- package/dist/bin/crank.js +34 -0
- package/dist/bin/relayer.d.ts +2 -0
- package/dist/bin/relayer.js +37 -0
- package/dist/chain.d.ts +14 -0
- package/dist/chain.js +48 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +61 -0
- package/dist/crank/gate.d.ts +2 -0
- package/dist/crank/gate.js +26 -0
- package/dist/crank/lending.d.ts +16 -0
- package/dist/crank/lending.js +144 -0
- package/dist/crank/prices.d.ts +11 -0
- package/dist/crank/prices.js +177 -0
- package/dist/crank/runner.d.ts +15 -0
- package/dist/crank/runner.js +30 -0
- package/dist/crank/shieldFees.d.ts +15 -0
- package/dist/crank/shieldFees.js +161 -0
- package/dist/epoch-root.d.ts +23 -0
- package/dist/epoch-root.js +98 -0
- package/dist/explorer.d.ts +41 -0
- package/dist/explorer.js +222 -0
- package/dist/gossip.d.ts +4 -0
- package/dist/gossip.js +77 -0
- package/dist/idl.d.ts +2 -0
- package/dist/idl.js +52 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +47 -0
- package/dist/lending.d.ts +82 -0
- package/dist/lending.js +164 -0
- package/dist/log.d.ts +27 -0
- package/dist/log.js +173 -0
- package/dist/lookupTable.d.ts +2 -0
- package/dist/lookupTable.js +47 -0
- package/dist/observer.d.ts +59 -0
- package/dist/observer.js +255 -0
- package/dist/receipt.d.ts +30 -0
- package/dist/receipt.js +27 -0
- package/dist/relayer/chain.d.ts +35 -0
- package/dist/relayer/chain.js +171 -0
- package/dist/relayer/errors.d.ts +8 -0
- package/dist/relayer/errors.js +29 -0
- package/dist/relayer/limits.d.ts +15 -0
- package/dist/relayer/limits.js +47 -0
- package/dist/relayer/relayer.d.ts +25 -0
- package/dist/relayer/relayer.js +54 -0
- package/dist/relayer/server.d.ts +8 -0
- package/dist/relayer/server.js +101 -0
- package/dist/relayer/settings.d.ts +23 -0
- package/dist/relayer/settings.js +69 -0
- package/dist/replay.d.ts +13 -0
- package/dist/replay.js +118 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.js +31 -0
- package/dist/settlement.d.ts +17 -0
- package/dist/settlement.js +96 -0
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.js +83 -0
- package/dist/shieldLimits.d.ts +15 -0
- package/dist/shieldLimits.js +24 -0
- package/dist/signer.d.ts +5 -0
- package/dist/signer.js +43 -0
- package/dist/token.d.ts +6 -0
- package/dist/token.js +33 -0
- package/package.json +36 -4
- package/README.md +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ShieldedRelayer = exports.refuse = void 0;
|
|
4
|
+
const relayer_1 = require("@zga/shielded-sdk/dist/relayer");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const refuse = (status, code, message) => ({ status, body: { code, message } });
|
|
7
|
+
exports.refuse = refuse;
|
|
8
|
+
class ShieldedRelayer {
|
|
9
|
+
constructor(chain, rate, inflight, log = () => { }) {
|
|
10
|
+
this.chain = chain;
|
|
11
|
+
this.rate = rate;
|
|
12
|
+
this.inflight = inflight;
|
|
13
|
+
this.log = log;
|
|
14
|
+
}
|
|
15
|
+
info() {
|
|
16
|
+
return { status: 200, body: this.chain.info() };
|
|
17
|
+
}
|
|
18
|
+
async unshield(raw, clientKey) {
|
|
19
|
+
if (!this.rate.allow(clientKey))
|
|
20
|
+
return (0, exports.refuse)(429, 'rate_limited', 'too many requests: try again in a minute');
|
|
21
|
+
const check = (0, relayer_1.checkUnshieldRequest)(this.chain.info(), raw);
|
|
22
|
+
if (!check.ok)
|
|
23
|
+
return (0, exports.refuse)(422, check.code, check.message);
|
|
24
|
+
const req = raw;
|
|
25
|
+
const key = req.nullifiers[0];
|
|
26
|
+
const entered = this.inflight.tryEnter(key);
|
|
27
|
+
if (entered === 'duplicate')
|
|
28
|
+
return (0, exports.refuse)(409, 'in_progress', 'this request is already being handled');
|
|
29
|
+
if (entered === 'busy')
|
|
30
|
+
return (0, exports.refuse)(503, 'busy', 'the relayer is handling as many requests as it takes: try again shortly');
|
|
31
|
+
try {
|
|
32
|
+
if (await this.chain.lowBalance()) {
|
|
33
|
+
this.log('refused: the relayer is short of SOL');
|
|
34
|
+
return (0, exports.refuse)(503, 'relayer_unavailable', 'the relayer cannot take requests at the moment');
|
|
35
|
+
}
|
|
36
|
+
if (await this.chain.anySpent(req.nullifiers))
|
|
37
|
+
return (0, exports.refuse)(422, 'already_spent', 'a note that this transaction spends has been spent');
|
|
38
|
+
const current = await this.chain.currentPool();
|
|
39
|
+
const { signature } = await this.chain.submit(req, current);
|
|
40
|
+
this.log(`relayed an exit of asset ${req.assetId}: ${signature}`);
|
|
41
|
+
return { status: 200, body: { signature } };
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e instanceof errors_1.RelayRefused)
|
|
45
|
+
return (0, exports.refuse)(422, e.code, e.message);
|
|
46
|
+
this.log(`submission failed: ${String(e?.message ?? e).slice(0, 160)}`);
|
|
47
|
+
return (0, exports.refuse)(502, 'submission_failed', 'the transaction could not be sent or confirmed: try again');
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
this.inflight.leave(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.ShieldedRelayer = ShieldedRelayer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { ShieldedRelayer } from './relayer';
|
|
3
|
+
export interface RelayerServerOptions {
|
|
4
|
+
maxBodyBytes: number;
|
|
5
|
+
trustProxy: boolean;
|
|
6
|
+
corsOrigin: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createShieldedRelayerServer(relayer: ShieldedRelayer, opts: RelayerServerOptions): http.Server;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createShieldedRelayerServer = createShieldedRelayerServer;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const relayer_1 = require("./relayer");
|
|
39
|
+
class BodyTooLarge extends Error {
|
|
40
|
+
}
|
|
41
|
+
function readBody(req, limit) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let size = 0;
|
|
45
|
+
let tooLarge = false;
|
|
46
|
+
req.on('data', (chunk) => {
|
|
47
|
+
size += chunk.length;
|
|
48
|
+
if (size > limit) {
|
|
49
|
+
tooLarge = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
chunks.push(chunk);
|
|
53
|
+
});
|
|
54
|
+
req.on('end', () => (tooLarge ? reject(new BodyTooLarge()) : resolve(Buffer.concat(chunks).toString('utf8'))));
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function createShieldedRelayerServer(relayer, opts) {
|
|
59
|
+
const server = http.createServer(async (req, res) => {
|
|
60
|
+
const send = (r) => {
|
|
61
|
+
res.writeHead(r.status, {
|
|
62
|
+
'content-type': 'application/json',
|
|
63
|
+
'cache-control': 'no-store',
|
|
64
|
+
'access-control-allow-origin': opts.corsOrigin,
|
|
65
|
+
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
66
|
+
'access-control-allow-headers': 'content-type',
|
|
67
|
+
});
|
|
68
|
+
res.end(JSON.stringify(r.body));
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
const url = (req.url ?? '').split('?')[0];
|
|
72
|
+
if (req.method === 'OPTIONS')
|
|
73
|
+
return send({ status: 204, body: {} });
|
|
74
|
+
if (req.method === 'GET' && url === '/v1/info')
|
|
75
|
+
return send(relayer.info());
|
|
76
|
+
if (req.method === 'GET' && url === '/healthz')
|
|
77
|
+
return send({ status: 200, body: { ok: true } });
|
|
78
|
+
if (req.method === 'POST' && url === '/v1/unshield') {
|
|
79
|
+
const text = await readBody(req, opts.maxBodyBytes);
|
|
80
|
+
let json;
|
|
81
|
+
try {
|
|
82
|
+
json = JSON.parse(text);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return send((0, relayer_1.refuse)(400, 'malformed', 'the body is not JSON'));
|
|
86
|
+
}
|
|
87
|
+
const forwarded = String(req.headers['x-forwarded-for'] ?? '').split(',')[0].trim();
|
|
88
|
+
const caller = opts.trustProxy && forwarded ? forwarded : req.socket.remoteAddress ?? 'unknown';
|
|
89
|
+
return send(await relayer.unshield(json, caller));
|
|
90
|
+
}
|
|
91
|
+
return send((0, relayer_1.refuse)(404, 'not_found', 'there is nothing at that address'));
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (e instanceof BodyTooLarge)
|
|
95
|
+
return send((0, relayer_1.refuse)(413, 'too_large', 'the request is larger than this relayer takes'));
|
|
96
|
+
return send((0, relayer_1.refuse)(500, 'internal', 'the relayer failed to handle the request'));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.setTimeout(30000);
|
|
100
|
+
return server;
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface RelayerAssetSetting {
|
|
2
|
+
assetId: number;
|
|
3
|
+
minFee: string;
|
|
4
|
+
feeBps: number;
|
|
5
|
+
}
|
|
6
|
+
export interface RelayerSettings {
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
assets: RelayerAssetSetting[];
|
|
10
|
+
lookupTables: string[];
|
|
11
|
+
operator: string | null;
|
|
12
|
+
priorityMicroLamports: number;
|
|
13
|
+
computeUnitLimit: number;
|
|
14
|
+
maxBodyBytes: number;
|
|
15
|
+
maxInFlight: number;
|
|
16
|
+
ratePerMinute: number;
|
|
17
|
+
minBalanceLamports: number;
|
|
18
|
+
trustProxy: boolean;
|
|
19
|
+
corsOrigin: string;
|
|
20
|
+
logRequests: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseAssets(text: string): RelayerAssetSetting[];
|
|
23
|
+
export declare function readRelayerSettings(env?: NodeJS.ProcessEnv): RelayerSettings;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseAssets = parseAssets;
|
|
4
|
+
exports.readRelayerSettings = readRelayerSettings;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
7
|
+
function intFrom(env, key, fallback, lo, hi) {
|
|
8
|
+
const v = env[key];
|
|
9
|
+
if (v === undefined || v === '')
|
|
10
|
+
return fallback;
|
|
11
|
+
const n = Number(v);
|
|
12
|
+
if (!Number.isInteger(n) || n < lo || n > hi)
|
|
13
|
+
throw new Error(`${key} must be an integer from ${lo} to ${hi}`);
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
function parseAssets(text) {
|
|
17
|
+
let list;
|
|
18
|
+
try {
|
|
19
|
+
list = JSON.parse(text);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error('RELAYER_ASSETS is not valid JSON');
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
25
|
+
throw new Error('RELAYER_ASSETS must list at least one asset');
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
return list.map((a, i) => {
|
|
28
|
+
if (!a || !Number.isInteger(a.assetId) || a.assetId < 0 || a.assetId > 255)
|
|
29
|
+
throw new Error(`RELAYER_ASSETS[${i}].assetId must be an integer from 0 to 255`);
|
|
30
|
+
if (seen.has(a.assetId))
|
|
31
|
+
throw new Error(`RELAYER_ASSETS lists asset ${a.assetId} twice`);
|
|
32
|
+
seen.add(a.assetId);
|
|
33
|
+
if (typeof a.minFee !== 'string' || !/^[0-9]{1,20}$/.test(a.minFee) || BigInt(a.minFee) > U64_MAX)
|
|
34
|
+
throw new Error(`RELAYER_ASSETS[${i}].minFee must be a decimal string of a 64-bit amount`);
|
|
35
|
+
if (!Number.isInteger(a.feeBps) || a.feeBps < 0 || a.feeBps > 10000)
|
|
36
|
+
throw new Error(`RELAYER_ASSETS[${i}].feeBps must be an integer from 0 to 10000`);
|
|
37
|
+
return { assetId: a.assetId, minFee: a.minFee, feeBps: a.feeBps };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function parseOperator(v) {
|
|
41
|
+
const s = (v ?? '').trim();
|
|
42
|
+
if (!s)
|
|
43
|
+
return null;
|
|
44
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s))
|
|
45
|
+
throw new Error('RELAYER_OPERATOR must be the NOP operator\'s public key, in base58');
|
|
46
|
+
return s;
|
|
47
|
+
}
|
|
48
|
+
function readRelayerSettings(env = process.env) {
|
|
49
|
+
const raw = env.RELAYER_ASSETS;
|
|
50
|
+
if (!raw)
|
|
51
|
+
throw new Error('RELAYER_ASSETS is not set: a JSON list of {assetId, minFee, feeBps}, or the path of a file that holds it');
|
|
52
|
+
const text = raw.trim().startsWith('[') ? raw : (0, fs_1.readFileSync)(raw, 'utf8');
|
|
53
|
+
return {
|
|
54
|
+
host: env.RELAYER_HOST ?? '127.0.0.1',
|
|
55
|
+
operator: parseOperator(env.RELAYER_OPERATOR),
|
|
56
|
+
port: intFrom(env, 'RELAYER_PORT', 8787, 1, 65535),
|
|
57
|
+
assets: parseAssets(text),
|
|
58
|
+
lookupTables: (env.RELAYER_LOOKUP_TABLES ?? '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
59
|
+
priorityMicroLamports: intFrom(env, 'RELAYER_PRIORITY_MICROLAMPORTS', 0, 0, 10000000),
|
|
60
|
+
computeUnitLimit: intFrom(env, 'RELAYER_COMPUTE_UNITS', 400000, 200000, 1400000),
|
|
61
|
+
maxBodyBytes: intFrom(env, 'RELAYER_MAX_BODY_BYTES', 16384, 1024, 1048576),
|
|
62
|
+
maxInFlight: intFrom(env, 'RELAYER_MAX_IN_FLIGHT', 4, 1, 64),
|
|
63
|
+
ratePerMinute: intFrom(env, 'RELAYER_RATE_PER_MINUTE', 20, 1, 6000),
|
|
64
|
+
minBalanceLamports: intFrom(env, 'RELAYER_MIN_BALANCE_LAMPORTS', 50000000, 0, 1000000000000),
|
|
65
|
+
trustProxy: env.RELAYER_TRUST_PROXY === '1',
|
|
66
|
+
corsOrigin: env.RELAYER_CORS_ORIGIN ?? '*',
|
|
67
|
+
logRequests: env.RELAYER_LOG_REQUESTS === '1',
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LogEntry } from './log';
|
|
2
|
+
export declare const MAX_MODULE_PAGES = 1024;
|
|
3
|
+
export declare const MAX_PLACES = 3;
|
|
4
|
+
export type ReplayResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
ranking: Uint8Array[] | null;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: string;
|
|
10
|
+
at?: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function replay(moduleBytes: Uint8Array, sessionId: Uint8Array, players: Uint8Array[], entries: LogEntry[]): Promise<ReplayResult>;
|
|
13
|
+
export declare function orderPlayers(players: Uint8Array[]): Uint8Array[];
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_PLACES = exports.MAX_MODULE_PAGES = void 0;
|
|
4
|
+
exports.replay = replay;
|
|
5
|
+
exports.orderPlayers = orderPlayers;
|
|
6
|
+
const sha256_1 = require("./sha256");
|
|
7
|
+
exports.MAX_MODULE_PAGES = 1024;
|
|
8
|
+
exports.MAX_PLACES = 3;
|
|
9
|
+
const OUTCOME_CAPACITY = 4 + 32 * exports.MAX_PLACES;
|
|
10
|
+
const fail = (reason, at) => ({ ok: false, reason, at });
|
|
11
|
+
function wasm() {
|
|
12
|
+
const w = globalThis.WebAssembly;
|
|
13
|
+
if (!w)
|
|
14
|
+
throw new Error('WebAssembly is not available');
|
|
15
|
+
return w;
|
|
16
|
+
}
|
|
17
|
+
function u32le(n) {
|
|
18
|
+
const b = new Uint8Array(4);
|
|
19
|
+
new DataView(b.buffer).setUint32(0, n, true);
|
|
20
|
+
return b;
|
|
21
|
+
}
|
|
22
|
+
const hex = sha256_1.toHex;
|
|
23
|
+
async function replay(moduleBytes, sessionId, players, entries) {
|
|
24
|
+
const W = wasm();
|
|
25
|
+
if (sessionId.length !== 32)
|
|
26
|
+
return fail('The session id is not 32 bytes');
|
|
27
|
+
if (players.some((p) => p.length !== 32))
|
|
28
|
+
return fail('A player key is not 32 bytes');
|
|
29
|
+
let module;
|
|
30
|
+
try {
|
|
31
|
+
module = await W.compile(moduleBytes);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return fail(`The module does not compile: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
if (W.Module.imports(module).length > 0)
|
|
37
|
+
return fail('The module imports host functions');
|
|
38
|
+
const exported = new Set(W.Module.exports(module).map((e) => e.name));
|
|
39
|
+
for (const name of ['memory', 'alloc', 'init', 'apply', 'outcome']) {
|
|
40
|
+
if (!exported.has(name))
|
|
41
|
+
return fail(`The module does not export ${name}`);
|
|
42
|
+
}
|
|
43
|
+
let instance;
|
|
44
|
+
try {
|
|
45
|
+
instance = await W.instantiate(module, {});
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return fail(`The module does not instantiate: ${e.message}`);
|
|
49
|
+
}
|
|
50
|
+
const ex = instance.exports;
|
|
51
|
+
const within = () => ex.memory.buffer.byteLength <= exports.MAX_MODULE_PAGES * 65536;
|
|
52
|
+
const put = (bytes) => {
|
|
53
|
+
const ptr = ex.alloc(bytes.length) >>> 0;
|
|
54
|
+
if (!within() || ptr + bytes.length > ex.memory.buffer.byteLength)
|
|
55
|
+
throw new Error('The module returned a region outside its memory');
|
|
56
|
+
new Uint8Array(ex.memory.buffer, ptr, bytes.length).set(bytes);
|
|
57
|
+
return ptr;
|
|
58
|
+
};
|
|
59
|
+
const known = new Set(players.map(hex));
|
|
60
|
+
try {
|
|
61
|
+
const initInput = new Uint8Array(32 + 4 + 32 * players.length);
|
|
62
|
+
initInput.set(sessionId, 0);
|
|
63
|
+
initInput.set(u32le(players.length), 32);
|
|
64
|
+
players.forEach((p, i) => initInput.set(p, 36 + 32 * i));
|
|
65
|
+
const rc = ex.init(put(initInput), initInput.length);
|
|
66
|
+
if (rc !== 0)
|
|
67
|
+
return fail(`The module refused the session (${rc})`);
|
|
68
|
+
for (let i = 0; i < entries.length; i++) {
|
|
69
|
+
const e = entries[i];
|
|
70
|
+
if (e.seq !== i)
|
|
71
|
+
return fail(`Log entry ${i} carries sequence ${e.seq}`, i);
|
|
72
|
+
if (!known.has(hex(e.player)))
|
|
73
|
+
return fail('An action names a player who is not in the session', i);
|
|
74
|
+
const input = new Uint8Array(32 + e.action.length);
|
|
75
|
+
input.set(e.player, 0);
|
|
76
|
+
input.set(e.action, 32);
|
|
77
|
+
const accepted = ex.apply(put(input), input.length);
|
|
78
|
+
if (accepted !== 0)
|
|
79
|
+
return fail(`The module rejected the action (${accepted})`, i);
|
|
80
|
+
if (!within())
|
|
81
|
+
return fail('The module grew its memory past the limit', i);
|
|
82
|
+
}
|
|
83
|
+
const out = ex.alloc(OUTCOME_CAPACITY) >>> 0;
|
|
84
|
+
if (!within() || out + OUTCOME_CAPACITY > ex.memory.buffer.byteLength)
|
|
85
|
+
return fail('The module returned a region outside its memory');
|
|
86
|
+
const written = ex.outcome(out);
|
|
87
|
+
if (written < 0)
|
|
88
|
+
return fail('The module has no outcome for this log');
|
|
89
|
+
if (written < 4 || written > OUTCOME_CAPACITY)
|
|
90
|
+
return fail('The module wrote an outcome of an invalid length');
|
|
91
|
+
const bytes = new Uint8Array(ex.memory.buffer, out, written);
|
|
92
|
+
const places = new DataView(bytes.buffer, bytes.byteOffset, written).getUint32(0, true);
|
|
93
|
+
if (places > exports.MAX_PLACES || written !== 4 + 32 * places)
|
|
94
|
+
return fail('The module wrote a malformed outcome');
|
|
95
|
+
if (places === 0)
|
|
96
|
+
return { ok: true, ranking: null };
|
|
97
|
+
const ranking = [];
|
|
98
|
+
for (let p = 0; p < places; p++)
|
|
99
|
+
ranking.push(bytes.slice(4 + 32 * p, 36 + 32 * p));
|
|
100
|
+
if (!ranking.every((k) => known.has(hex(k))))
|
|
101
|
+
return fail('The outcome names a player who is not in the session');
|
|
102
|
+
if (new Set(ranking.map(hex)).size !== places)
|
|
103
|
+
return fail('The outcome names a player twice');
|
|
104
|
+
return { ok: true, ranking };
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return fail(`The module trapped: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function orderPlayers(players) {
|
|
111
|
+
return [...players].sort((a, b) => {
|
|
112
|
+
const n = Math.min(a.length, b.length);
|
|
113
|
+
for (let i = 0; i < n; i++)
|
|
114
|
+
if (a[i] !== b[i])
|
|
115
|
+
return a[i] - b[i];
|
|
116
|
+
return a.length - b.length;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
2
|
+
export interface CreateSessionArgs {
|
|
3
|
+
gameId: Uint8Array;
|
|
4
|
+
entryFee: bigint;
|
|
5
|
+
maxPlayers: number;
|
|
6
|
+
minPlayers: number;
|
|
7
|
+
prizeSplitBps?: number[];
|
|
8
|
+
fundingTimeoutSlots?: bigint;
|
|
9
|
+
sessionId?: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSession(env: {
|
|
12
|
+
program: any;
|
|
13
|
+
connection: Connection;
|
|
14
|
+
host: Keypair;
|
|
15
|
+
guild: PublicKey;
|
|
16
|
+
}, a: CreateSessionArgs): Promise<{
|
|
17
|
+
session: PublicKey;
|
|
18
|
+
sessionId: Uint8Array;
|
|
19
|
+
signature: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function depositEntry(env: {
|
|
22
|
+
program: any;
|
|
23
|
+
player: Keypair;
|
|
24
|
+
}, args: {
|
|
25
|
+
session: PublicKey;
|
|
26
|
+
gameId: Uint8Array;
|
|
27
|
+
entryFee: bigint;
|
|
28
|
+
zgaMint: PublicKey;
|
|
29
|
+
}): Promise<string>;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSession = createSession;
|
|
4
|
+
exports.depositEntry = depositEntry;
|
|
5
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
6
|
+
const anchor_1 = require("@coral-xyz/anchor");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const addresses_1 = require("./addresses");
|
|
9
|
+
const token_1 = require("./token");
|
|
10
|
+
async function createSession(env, a) {
|
|
11
|
+
const addr = new addresses_1.Addresses(env.program.programId);
|
|
12
|
+
const sessionId = a.sessionId ?? (0, crypto_1.randomBytes)(32);
|
|
13
|
+
const session = addr.session(sessionId);
|
|
14
|
+
const signature = await env.program.methods.createSession(Array.from(sessionId), Array.from(a.gameId), new anchor_1.BN(a.entryFee.toString()), a.maxPlayers, a.minPlayers, env.guild, { minimum: {} }, new anchor_1.BN((a.fundingTimeoutSlots ?? 999999999n).toString()), a.prizeSplitBps ?? []).accounts({
|
|
15
|
+
session, vault: addr.vault(session), zgapAuthority: addr.zgapAuthority, mint: addr.zgapMint,
|
|
16
|
+
gameManifest: addr.gameManifest(a.gameId), rateLimit: addr.rateLimit(env.guild), protocolConfig: addr.config,
|
|
17
|
+
validatorRegistry: null, subsidyPool: null, guildAccount: env.guild, prizeVault: addr.prizeVault(session),
|
|
18
|
+
host: env.host.publicKey, tokenProgram: token_1.TOKEN_PROGRAM_ID, systemProgram: web3_js_1.SystemProgram.programId, rent: web3_js_1.SYSVAR_RENT_PUBKEY,
|
|
19
|
+
}).signers([env.host]).rpc();
|
|
20
|
+
return { session, sessionId, signature };
|
|
21
|
+
}
|
|
22
|
+
async function depositEntry(env, args) {
|
|
23
|
+
const addr = new addresses_1.Addresses(env.program.programId);
|
|
24
|
+
const user = env.player.publicKey;
|
|
25
|
+
return env.program.methods.depositEntry(new anchor_1.BN(args.entryFee.toString())).accounts({
|
|
26
|
+
user, session: args.session, playerAccount: addr.player(args.session, user), vault: addr.vault(args.session),
|
|
27
|
+
userTokenAccount: (0, token_1.associatedTokenAddress)(addr.zgapMint, user), gameManifest: addr.gameManifest(args.gameId),
|
|
28
|
+
playerZgaAccount: (0, token_1.associatedTokenAddress)(args.zgaMint, user), agentBond: addr.agentBond(user),
|
|
29
|
+
tokenProgram: token_1.TOKEN_PROGRAM_ID, systemProgram: web3_js_1.SystemProgram.programId,
|
|
30
|
+
}).signers([env.player]).rpc();
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AddressLookupTableAccount, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
|
2
|
+
import { Addresses } from './addresses';
|
|
3
|
+
import { EnclaveSigner } from './signer';
|
|
4
|
+
export interface SettleEnv {
|
|
5
|
+
program: any;
|
|
6
|
+
connection: Connection;
|
|
7
|
+
payer: Keypair;
|
|
8
|
+
enclave: EnclaveSigner;
|
|
9
|
+
lookupTable: AddressLookupTableAccount;
|
|
10
|
+
}
|
|
11
|
+
export interface SettleArgs {
|
|
12
|
+
session: PublicKey;
|
|
13
|
+
ranking: PublicKey[] | null;
|
|
14
|
+
logRoot?: Uint8Array | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function settlementStaticAccounts(connection: Connection, addresses: Addresses, guild: PublicKey, enclave: PublicKey, operator: PublicKey): Promise<PublicKey[]>;
|
|
17
|
+
export declare function settleSession(env: SettleEnv, args: SettleArgs): Promise<string>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.settlementStaticAccounts = settlementStaticAccounts;
|
|
4
|
+
exports.settleSession = settleSession;
|
|
5
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
6
|
+
const anchor_1 = require("@coral-xyz/anchor");
|
|
7
|
+
const addresses_1 = require("./addresses");
|
|
8
|
+
const receipt_1 = require("./receipt");
|
|
9
|
+
const token_1 = require("./token");
|
|
10
|
+
async function settlementStaticAccounts(connection, addresses, guild, enclave, operator) {
|
|
11
|
+
const zgaMint = await (0, addresses_1.zgaMintOf)(connection, addresses);
|
|
12
|
+
return [
|
|
13
|
+
addresses.priceCache(0), addresses.priceCache(1), addresses.zgapSolVault, addresses.zgapReserveSol,
|
|
14
|
+
addresses.config, addresses.zgapAuthority, addresses.zgapMint, zgaMint, addresses.zgapCollateralVault, addresses.zgapReserve,
|
|
15
|
+
(0, token_1.associatedTokenAddress)(zgaMint, addresses.zgapReserve), addresses.lpLock, addresses.yieldVault, addresses.rewardDistributor,
|
|
16
|
+
(0, token_1.associatedTokenAddress)(addresses.zgapMint, operator), guild, addresses.rateLimit(guild), addresses.attestationCert(enclave),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
async function settleSession(env, args) {
|
|
20
|
+
const { program, connection, payer, enclave, lookupTable } = env;
|
|
21
|
+
const addr = new addresses_1.Addresses(program.programId);
|
|
22
|
+
const session = await program.account.session.fetch(args.session);
|
|
23
|
+
const config = await program.account.protocolConfig.fetch(addr.config);
|
|
24
|
+
const guildAccount = await program.account.guildAccount.fetch(session.guild);
|
|
25
|
+
const gameId = Uint8Array.from(session.gameId);
|
|
26
|
+
const manifest = await program.account.gameManifest.fetch(addr.gameManifest(gameId));
|
|
27
|
+
const places = session.prizePlaces;
|
|
28
|
+
const draw = args.ranking === null;
|
|
29
|
+
if (!draw) {
|
|
30
|
+
const ranking = args.ranking;
|
|
31
|
+
if (ranking.length !== places)
|
|
32
|
+
throw new Error(`The session has ${places} prize place(s)`);
|
|
33
|
+
if (new Set(ranking.map((k) => k.toBase58())).size !== places)
|
|
34
|
+
throw new Error('A prize place is named twice');
|
|
35
|
+
}
|
|
36
|
+
const vault = addr.vault(args.session);
|
|
37
|
+
const entryFees = BigInt((await connection.getTokenAccountBalance(vault)).value.amount);
|
|
38
|
+
const terms = (0, receipt_1.settlementTerms)({
|
|
39
|
+
entryFees,
|
|
40
|
+
protocolTaxBps: BigInt(config.protocolTaxBps.toString()),
|
|
41
|
+
guildFeeBps: BigInt(session.guildFeeBps.toString()),
|
|
42
|
+
royaltyBps: BigInt(session.hostRoyaltyBps.toString()),
|
|
43
|
+
draw,
|
|
44
|
+
});
|
|
45
|
+
const slot = await connection.getSlot();
|
|
46
|
+
const blockTime = (await connection.getBlockTime(slot)) ?? Math.floor(Date.now() / 1000);
|
|
47
|
+
const ranking = args.ranking ?? [];
|
|
48
|
+
const receipt = {
|
|
49
|
+
sessionId: Uint8Array.from(session.sessionId),
|
|
50
|
+
guild: session.guild, host: session.host,
|
|
51
|
+
entryFees, ...terms,
|
|
52
|
+
timestamp: BigInt(blockTime),
|
|
53
|
+
winner: ranking[0] ?? null, second: ranking[1] ?? null, third: ranking[2] ?? null,
|
|
54
|
+
logRoot: args.logRoot ?? null,
|
|
55
|
+
};
|
|
56
|
+
const hash = (0, receipt_1.receiptHash)(receipt);
|
|
57
|
+
const signature = await enclave.sign(hash);
|
|
58
|
+
const signatureIx = web3_js_1.Ed25519Program.createInstructionWithPublicKey({ publicKey: enclave.publicKey, message: hash, signature });
|
|
59
|
+
const argument = {
|
|
60
|
+
sessionId: Array.from(receipt.sessionId), guild: receipt.guild, host: receipt.host,
|
|
61
|
+
entryFees: new anchor_1.BN(receipt.entryFees.toString()), prizePool: new anchor_1.BN(receipt.prizePool.toString()),
|
|
62
|
+
guildFee: new anchor_1.BN(receipt.guildFee.toString()), hostRoyalty: new anchor_1.BN(receipt.hostRoyalty.toString()),
|
|
63
|
+
protocolTax: new anchor_1.BN(receipt.protocolTax.toString()), timestamp: new anchor_1.BN(receipt.timestamp.toString()),
|
|
64
|
+
second: receipt.second, third: receipt.third, winner: receipt.winner,
|
|
65
|
+
logRoot: receipt.logRoot ? Array.from(receipt.logRoot) : null,
|
|
66
|
+
};
|
|
67
|
+
const zgaMint = await (0, addresses_1.zgaMintOf)(connection, addr);
|
|
68
|
+
const creator = manifest.creator;
|
|
69
|
+
const gameManifest = addr.gameManifest(gameId);
|
|
70
|
+
const settleIx = await program.methods.settleSession(argument).accounts({
|
|
71
|
+
protocolRedeem: {
|
|
72
|
+
zgaPriceCache: addr.priceCache(0), solPriceCache: addr.priceCache(1),
|
|
73
|
+
solVault: addr.zgapSolVault, reserveSolAccount: addr.zgapReserveSol,
|
|
74
|
+
},
|
|
75
|
+
session: args.session, vault, yieldVault: addr.yieldVault, rewardDistributor: addr.rewardDistributor, protocolConfig: addr.config,
|
|
76
|
+
zgapAuthority: addr.zgapAuthority, zgapMint: addr.zgapMint, zgaMint, zgapCollateralVault: addr.zgapCollateralVault,
|
|
77
|
+
zgapReserve: addr.zgapReserve, zgapReserveZgaAccount: (0, token_1.associatedTokenAddress)(zgaMint, addr.zgapReserve),
|
|
78
|
+
lpLockTokenAccount: addr.lpLock, guildTokenAccount: (0, token_1.associatedTokenAddress)(addr.zgapMint, guildAccount.operator),
|
|
79
|
+
gameManifest, gameCreatorAccount: addr.gameCreator(creator, gameId), royaltyVault: addr.royaltyVault(creator, gameId),
|
|
80
|
+
prizeVault: addr.prizeVault(args.session), guildAccount: session.guild,
|
|
81
|
+
tokenProgram: token_1.TOKEN_PROGRAM_ID, ixSysvar: web3_js_1.SYSVAR_INSTRUCTIONS_PUBKEY,
|
|
82
|
+
}).remainingAccounts([
|
|
83
|
+
{ pubkey: gameManifest, isSigner: false, isWritable: false },
|
|
84
|
+
{ pubkey: addr.rateLimit(session.guild), isSigner: false, isWritable: true },
|
|
85
|
+
{ pubkey: addr.attestationCert(new web3_js_1.PublicKey(enclave.publicKey)), isSigner: false, isWritable: false },
|
|
86
|
+
]).instruction();
|
|
87
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
|
|
88
|
+
const message = new web3_js_1.TransactionMessage({
|
|
89
|
+
payerKey: payer.publicKey, recentBlockhash: blockhash, instructions: [signatureIx, settleIx],
|
|
90
|
+
}).compileToV0Message([lookupTable]);
|
|
91
|
+
const tx = new web3_js_1.VersionedTransaction(message);
|
|
92
|
+
tx.sign([payer]);
|
|
93
|
+
const sent = await connection.sendTransaction(tx);
|
|
94
|
+
await connection.confirmTransaction({ signature: sent, blockhash, lastValidBlockHeight }, 'confirmed');
|
|
95
|
+
return sent;
|
|
96
|
+
}
|
package/dist/sha256.d.ts
ADDED
package/dist/sha256.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sha256 = sha256;
|
|
4
|
+
exports.toHex = toHex;
|
|
5
|
+
exports.fromHex = fromHex;
|
|
6
|
+
const K = new Uint32Array([
|
|
7
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
8
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
9
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
10
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
11
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
12
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
13
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
14
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
15
|
+
]);
|
|
16
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
17
|
+
function sha256(...parts) {
|
|
18
|
+
const length = parts.reduce((n, p) => n + p.length, 0);
|
|
19
|
+
const padded = new Uint8Array(Math.ceil((length + 9) / 64) * 64);
|
|
20
|
+
let at = 0;
|
|
21
|
+
for (const p of parts) {
|
|
22
|
+
padded.set(p, at);
|
|
23
|
+
at += p.length;
|
|
24
|
+
}
|
|
25
|
+
padded[length] = 0x80;
|
|
26
|
+
const view = new DataView(padded.buffer);
|
|
27
|
+
view.setUint32(padded.length - 8, Math.floor(length / 536870912), false);
|
|
28
|
+
view.setUint32(padded.length - 4, (length << 3) >>> 0, false);
|
|
29
|
+
const h = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
30
|
+
const w = new Uint32Array(64);
|
|
31
|
+
for (let block = 0; block < padded.length; block += 64) {
|
|
32
|
+
for (let i = 0; i < 16; i++)
|
|
33
|
+
w[i] = view.getUint32(block + i * 4, false);
|
|
34
|
+
for (let i = 16; i < 64; i++) {
|
|
35
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
36
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
37
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
38
|
+
}
|
|
39
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
40
|
+
for (let i = 0; i < 64; i++) {
|
|
41
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
42
|
+
const ch = (e & f) ^ (~e & g);
|
|
43
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
|
|
44
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
45
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
46
|
+
const t2 = (S0 + maj) >>> 0;
|
|
47
|
+
hh = g;
|
|
48
|
+
g = f;
|
|
49
|
+
f = e;
|
|
50
|
+
e = (d + t1) >>> 0;
|
|
51
|
+
d = c;
|
|
52
|
+
c = b;
|
|
53
|
+
b = a;
|
|
54
|
+
a = (t1 + t2) >>> 0;
|
|
55
|
+
}
|
|
56
|
+
h[0] = (h[0] + a) >>> 0;
|
|
57
|
+
h[1] = (h[1] + b) >>> 0;
|
|
58
|
+
h[2] = (h[2] + c) >>> 0;
|
|
59
|
+
h[3] = (h[3] + d) >>> 0;
|
|
60
|
+
h[4] = (h[4] + e) >>> 0;
|
|
61
|
+
h[5] = (h[5] + f) >>> 0;
|
|
62
|
+
h[6] = (h[6] + g) >>> 0;
|
|
63
|
+
h[7] = (h[7] + hh) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
const out = new Uint8Array(32);
|
|
66
|
+
const o = new DataView(out.buffer);
|
|
67
|
+
h.forEach((v, i) => o.setUint32(i * 4, v, false));
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function toHex(bytes) {
|
|
71
|
+
let s = '';
|
|
72
|
+
for (const b of bytes)
|
|
73
|
+
s += b.toString(16).padStart(2, '0');
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
function fromHex(hex) {
|
|
77
|
+
if (hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex))
|
|
78
|
+
throw new Error('Malformed hex');
|
|
79
|
+
const out = new Uint8Array(hex.length / 2);
|
|
80
|
+
for (let i = 0; i < out.length; i++)
|
|
81
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
82
|
+
return out;
|
|
83
|
+
}
|