@gl-36/sdk 0.0.0-stage → 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/dist/account.d.ts +35 -0
  4. package/dist/account.d.ts.map +1 -0
  5. package/dist/account.js +63 -0
  6. package/dist/account.js.map +1 -0
  7. package/dist/constants.d.ts +41 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +39 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +33 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +77 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +10 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/instructions.d.ts +27 -0
  20. package/dist/instructions.d.ts.map +1 -0
  21. package/dist/instructions.js +137 -0
  22. package/dist/instructions.js.map +1 -0
  23. package/dist/logs.d.ts +32 -0
  24. package/dist/logs.d.ts.map +1 -0
  25. package/dist/logs.js +63 -0
  26. package/dist/logs.js.map +1 -0
  27. package/dist/pda.d.ts +3 -0
  28. package/dist/pda.d.ts.map +1 -0
  29. package/dist/pda.js +14 -0
  30. package/dist/pda.js.map +1 -0
  31. package/dist/price.d.ts +5 -0
  32. package/dist/price.d.ts.map +1 -0
  33. package/dist/price.js +38 -0
  34. package/dist/price.js.map +1 -0
  35. package/dist/rpc.d.ts +26 -0
  36. package/dist/rpc.d.ts.map +1 -0
  37. package/dist/rpc.js +70 -0
  38. package/dist/rpc.js.map +1 -0
  39. package/dist/seed.d.ts +9 -0
  40. package/dist/seed.d.ts.map +1 -0
  41. package/dist/seed.js +18 -0
  42. package/dist/seed.js.map +1 -0
  43. package/package.json +56 -4
  44. package/src/account.ts +109 -0
  45. package/src/constants.ts +50 -0
  46. package/src/errors.ts +89 -0
  47. package/src/index.ts +9 -0
  48. package/src/instructions.ts +186 -0
  49. package/src/logs.ts +77 -0
  50. package/src/pda.ts +14 -0
  51. package/src/price.ts +39 -0
  52. package/src/rpc.ts +127 -0
  53. package/src/seed.ts +30 -0
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./constants.ts";
2
+ export * from "./account.ts";
3
+ export * from "./pda.ts";
4
+ export * from "./instructions.ts";
5
+ export * from "./errors.ts";
6
+ export * from "./logs.ts";
7
+ export * from "./price.ts";
8
+ export * from "./seed.ts";
9
+ export * from "./rpc.ts";
@@ -0,0 +1,186 @@
1
+ import { AccountRole, type Address, type Instruction, type TransactionSigner } from "@solana/kit";
2
+ import { MAX_FEE_BPS, MAX_MINT_BATCH, MAX_PRICE, MAX_SUPPLY, SLOT_HASHES, SYSTEM_PROGRAM } from "./constants.ts";
3
+
4
+ export const INSTRUCTION = {
5
+ init: 0,
6
+ mint: 1,
7
+ transfer: 2,
8
+ list: 3,
9
+ buy: 4,
10
+ close: 5,
11
+ withdrawExcess: 6,
12
+ settle: 7,
13
+ } as const;
14
+
15
+ export interface InitArgs {
16
+ nonce: number;
17
+ maxSupply: number;
18
+ feeBps: number;
19
+ mintPrice: bigint;
20
+ generatorHash: Uint8Array;
21
+ }
22
+
23
+ class Writer {
24
+ buf: Uint8Array;
25
+ view: DataView;
26
+ off = 0;
27
+ constructor(len: number) {
28
+ this.buf = new Uint8Array(len);
29
+ this.view = new DataView(this.buf.buffer);
30
+ }
31
+ u8(v: number) {
32
+ this.view.setUint8(this.off, v);
33
+ this.off += 1;
34
+ return this;
35
+ }
36
+ u16(v: number) {
37
+ this.view.setUint16(this.off, v, true);
38
+ this.off += 2;
39
+ return this;
40
+ }
41
+ u32(v: number) {
42
+ this.view.setUint32(this.off, v, true);
43
+ this.off += 4;
44
+ return this;
45
+ }
46
+ u64(v: bigint) {
47
+ this.view.setBigUint64(this.off, v, true);
48
+ this.off += 8;
49
+ return this;
50
+ }
51
+ bytes(b: Uint8Array) {
52
+ this.buf.set(b, this.off);
53
+ this.off += b.length;
54
+ return this;
55
+ }
56
+ }
57
+
58
+ function int(name: string, v: number, min: number, max: number) {
59
+ if (!Number.isInteger(v) || v < min || v > max) throw new RangeError(`${name} must be an integer from ${min} to ${max}, got ${v}`);
60
+ }
61
+
62
+ function bytes32(name: string, b: Uint8Array) {
63
+ if (b.length !== 32) throw new RangeError(`${name} must be 32 bytes, got ${b.length}`);
64
+ }
65
+
66
+ const signer = (s: TransactionSigner, writable: boolean) => ({
67
+ address: s.address,
68
+ role: writable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER,
69
+ signer: s,
70
+ });
71
+ const w = (address: Address) => ({ address, role: AccountRole.WRITABLE });
72
+ const r = (address: Address) => ({ address, role: AccountRole.READONLY });
73
+
74
+ export function initIx(
75
+ programId: Address,
76
+ creator: TransactionSigner,
77
+ collection: Address,
78
+ treasury: Address,
79
+ a: InitArgs,
80
+ ): Instruction {
81
+ int("nonce", a.nonce, 0, 0xffff_ffff);
82
+ int("maxSupply", a.maxSupply, 1, MAX_SUPPLY);
83
+ int("feeBps", a.feeBps, 0, MAX_FEE_BPS);
84
+ if (a.mintPrice < 0n || a.mintPrice > 0xffff_ffff_ffff_ffffn) throw new RangeError(`mintPrice must be a u64, got ${a.mintPrice}`);
85
+ bytes32("generatorHash", a.generatorHash);
86
+ const data = new Writer(49)
87
+ .u8(INSTRUCTION.init)
88
+ .u32(a.nonce)
89
+ .u16(a.maxSupply)
90
+ .u16(a.feeBps)
91
+ .u64(a.mintPrice)
92
+ .bytes(a.generatorHash).buf;
93
+ return {
94
+ programAddress: programId,
95
+ accounts: [signer(creator, true), w(collection), r(treasury), r(SYSTEM_PROGRAM)],
96
+ data,
97
+ };
98
+ }
99
+
100
+ export function mintIx(
101
+ programId: Address,
102
+ minter: TransactionSigner,
103
+ collection: Address,
104
+ creator: Address,
105
+ count: number,
106
+ ): Instruction {
107
+ int("count", count, 1, MAX_MINT_BATCH);
108
+ return {
109
+ programAddress: programId,
110
+ accounts: [signer(minter, true), w(collection), w(creator), r(SYSTEM_PROGRAM)],
111
+ data: new Writer(2).u8(INSTRUCTION.mint).u8(count).buf,
112
+ };
113
+ }
114
+
115
+ export function transferIx(
116
+ programId: Address,
117
+ owner: TransactionSigner,
118
+ collection: Address,
119
+ newOwner: Address,
120
+ id: number,
121
+ ): Instruction {
122
+ int("id", id, 0, MAX_SUPPLY - 1);
123
+ return {
124
+ programAddress: programId,
125
+ accounts: [signer(owner, false), w(collection), r(newOwner)],
126
+ data: new Writer(3).u8(INSTRUCTION.transfer).u16(id).buf,
127
+ };
128
+ }
129
+
130
+ export function listIx(
131
+ programId: Address,
132
+ owner: TransactionSigner,
133
+ collection: Address,
134
+ id: number,
135
+ price: number,
136
+ ): Instruction {
137
+ int("id", id, 0, MAX_SUPPLY - 1);
138
+ int("price", price, 0, MAX_PRICE);
139
+ return {
140
+ programAddress: programId,
141
+ accounts: [signer(owner, false), w(collection)],
142
+ data: new Writer(7).u8(INSTRUCTION.list).u16(id).u32(price).buf,
143
+ };
144
+ }
145
+
146
+ export function buyIx(
147
+ programId: Address,
148
+ buyer: TransactionSigner,
149
+ collection: Address,
150
+ seller: Address,
151
+ treasury: Address,
152
+ id: number,
153
+ maxPrice: number,
154
+ ): Instruction {
155
+ int("id", id, 0, MAX_SUPPLY - 1);
156
+ int("maxPrice", maxPrice, 1, MAX_PRICE);
157
+ return {
158
+ programAddress: programId,
159
+ accounts: [signer(buyer, true), w(collection), w(seller), w(treasury), r(SYSTEM_PROGRAM)],
160
+ data: new Writer(7).u8(INSTRUCTION.buy).u16(id).u32(maxPrice).buf,
161
+ };
162
+ }
163
+
164
+ export function closeIx(programId: Address, creator: TransactionSigner, collection: Address): Instruction {
165
+ return {
166
+ programAddress: programId,
167
+ accounts: [signer(creator, false), w(collection)],
168
+ data: new Uint8Array([INSTRUCTION.close]),
169
+ };
170
+ }
171
+
172
+ export function settleIx(programId: Address, collection: Address): Instruction {
173
+ return {
174
+ programAddress: programId,
175
+ accounts: [w(collection), r(SLOT_HASHES)],
176
+ data: new Uint8Array([INSTRUCTION.settle]),
177
+ };
178
+ }
179
+
180
+ export function withdrawExcessIx(programId: Address, creator: TransactionSigner, collection: Address): Instruction {
181
+ return {
182
+ programAddress: programId,
183
+ accounts: [signer(creator, true), w(collection)],
184
+ data: new Uint8Array([INSTRUCTION.withdrawExcess]),
185
+ };
186
+ }
package/src/logs.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { Address } from "@solana/kit";
2
+
3
+ export type Gl36Event =
4
+ | { type: "init"; maxSupply: number }
5
+ | { type: "mint"; firstId: number; count: number }
6
+ | { type: "transfer"; id: number }
7
+ | { type: "list"; id: number; price: number }
8
+ | { type: "buy"; id: number; price: number }
9
+ | { type: "close"; minted: number }
10
+ | { type: "settle" }
11
+ | { type: "rearm" }
12
+ | { type: "withdraw"; lamports: bigint };
13
+
14
+ const INVOKE = /^Program (\w+) invoke \[\d+\]$/;
15
+ const EXIT = /^Program (\w+) (success|failed)/;
16
+ const LOG = "Program log: ";
17
+
18
+ const ints = (parts: string[], n: number) =>
19
+ parts.length === n && parts.every((p) => /^\d+$/.test(p)) ? parts.map(Number) : null;
20
+
21
+ function parseEvent(message: string): Gl36Event | null {
22
+ const [kind, ...rest] = message.trim().split(/\s+/);
23
+ switch (kind) {
24
+ case "init": {
25
+ const v = ints(rest, 1);
26
+ return v && { type: "init", maxSupply: v[0] };
27
+ }
28
+ case "mint": {
29
+ const v = ints(rest, 2);
30
+ return v && { type: "mint", firstId: v[0], count: v[1] };
31
+ }
32
+ case "xfer": {
33
+ const v = ints(rest, 1);
34
+ return v && { type: "transfer", id: v[0] };
35
+ }
36
+ case "list": {
37
+ const v = ints(rest, 2);
38
+ return v && { type: "list", id: v[0], price: v[1] };
39
+ }
40
+ case "buy": {
41
+ const v = ints(rest, 2);
42
+ return v && { type: "buy", id: v[0], price: v[1] };
43
+ }
44
+ case "close": {
45
+ const v = ints(rest, 1);
46
+ return v && { type: "close", minted: v[0] };
47
+ }
48
+ case "settle":
49
+ return rest.length === 0 ? { type: "settle" } : null;
50
+ case "rearm":
51
+ return rest.length === 0 ? { type: "rearm" } : null;
52
+ case "withdraw":
53
+ return rest.length === 1 && /^\d+$/.test(rest[0]) ? { type: "withdraw", lamports: BigInt(rest[0]) } : null;
54
+ default:
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function parseLogs(logs: readonly string[], programId: Address): Gl36Event[] {
60
+ const stack: string[] = [];
61
+ const events: Gl36Event[] = [];
62
+ for (const line of logs) {
63
+ const invoke = INVOKE.exec(line);
64
+ if (invoke) {
65
+ stack.push(invoke[1]);
66
+ continue;
67
+ }
68
+ if (EXIT.test(line)) {
69
+ stack.pop();
70
+ continue;
71
+ }
72
+ if (stack.at(-1) !== programId || !line.startsWith(LOG)) continue;
73
+ const event = parseEvent(line.slice(LOG.length));
74
+ if (event) events.push(event);
75
+ }
76
+ return events;
77
+ }
package/src/pda.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { getAddressEncoder, getProgramDerivedAddress, type Address } from "@solana/kit";
2
+
3
+ export async function findCollection(programId: Address, creator: Address, nonce: number): Promise<[Address, number]> {
4
+ if (!Number.isInteger(nonce) || nonce < 0 || nonce > 0xffff_ffff) {
5
+ throw new RangeError(`nonce must be a u32, got ${nonce}`);
6
+ }
7
+ const n = new Uint8Array(4);
8
+ new DataView(n.buffer).setUint32(0, nonce, true);
9
+ const [pda, bump] = await getProgramDerivedAddress({
10
+ programAddress: programId,
11
+ seeds: [new TextEncoder().encode("col"), getAddressEncoder().encode(creator), n],
12
+ });
13
+ return [pda, bump];
14
+ }
package/src/price.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { MAX_PRICE, PRICE_UNIT } from "./constants.ts";
2
+
3
+ const DECIMALS = 6;
4
+
5
+ function check(price: number) {
6
+ if (!Number.isInteger(price) || price < 0 || price > MAX_PRICE) {
7
+ throw new RangeError(`price must be an integer from 0 to ${MAX_PRICE} units of 1,000 lamports, got ${price}`);
8
+ }
9
+ }
10
+
11
+ export function priceToLamports(price: number): bigint {
12
+ check(price);
13
+ return BigInt(price) * PRICE_UNIT;
14
+ }
15
+
16
+ export function lamportsToPrice(lamports: bigint): number {
17
+ if (lamports % PRICE_UNIT !== 0n) throw new RangeError(`${lamports} lamports is not a whole number of price units`);
18
+ const price = lamports / PRICE_UNIT;
19
+ if (price < 0n || price > BigInt(MAX_PRICE)) throw new RangeError(`${lamports} lamports is outside the u32 price range`);
20
+ return Number(price);
21
+ }
22
+
23
+ export function solToPrice(sol: string | number): number {
24
+ const text = typeof sol === "number" ? sol.toFixed(DECIMALS) : sol.trim();
25
+ const m = /^(\d*)(?:[.,](\d*))?$/.exec(text);
26
+ if (!m || (m[1] === "" && !m[2])) throw new RangeError(`not a SOL amount: ${sol}`);
27
+ const frac = (m[2] ?? "").replace(/0+$/, "");
28
+ if (frac.length > DECIMALS) throw new RangeError(`prices go down to 0.000001 SOL, got ${sol}`);
29
+ const price = Number(BigInt(m[1] || "0") * 10n ** BigInt(DECIMALS) + BigInt(frac.padEnd(DECIMALS, "0") || "0"));
30
+ check(price);
31
+ return price;
32
+ }
33
+
34
+ export function priceToSol(price: number): string {
35
+ check(price);
36
+ const whole = Math.floor(price / 10 ** DECIMALS);
37
+ const frac = String(price % 10 ** DECIMALS).padStart(DECIMALS, "0").replace(/0+$/, "");
38
+ return frac ? `${whole}.${frac}` : String(whole);
39
+ }
package/src/rpc.ts ADDED
@@ -0,0 +1,127 @@
1
+ import {
2
+ getBase58Decoder,
3
+ getBase64Encoder,
4
+ type Address,
5
+ type Base58EncodedBytes,
6
+ type Commitment,
7
+ type GetAccountInfoApi,
8
+ type GetMinimumBalanceForRentExemptionApi,
9
+ type GetProgramAccountsApi,
10
+ type Rpc,
11
+ } from "@solana/kit";
12
+ import {
13
+ decodeCollection,
14
+ decodeHeader,
15
+ decodeSlot,
16
+ slotOffset,
17
+ type Collection,
18
+ type CollectionHeader,
19
+ type Slot,
20
+ } from "./account.ts";
21
+ import { ACCOUNT_OVERHEAD, COLLECTION_TAG, HEADER_LEN, HEADER_OFFSET, LAYOUT_VERSION, SLOT_LEN } from "./constants.ts";
22
+
23
+ export interface FetchConfig {
24
+ commitment?: Commitment;
25
+ programId?: Address;
26
+ }
27
+
28
+ export interface FetchedCollection {
29
+ address: Address;
30
+ lamports: bigint;
31
+ data: Uint8Array;
32
+ collection: Collection;
33
+ }
34
+
35
+ const bytes = (base64: string) => new Uint8Array(getBase64Encoder().encode(base64));
36
+
37
+ function owned(owner: Address, programId: Address | undefined, address: Address) {
38
+ if (programId !== undefined && owner !== programId) {
39
+ throw new Error(`${address} is owned by ${owner}, not by the GL-36 program ${programId}`);
40
+ }
41
+ }
42
+
43
+ export async function fetchCollection(
44
+ rpc: Rpc<GetAccountInfoApi>,
45
+ address: Address,
46
+ config: FetchConfig = {},
47
+ ): Promise<FetchedCollection | null> {
48
+ const { value } = await rpc
49
+ .getAccountInfo(address, { encoding: "base64", commitment: config.commitment ?? "confirmed" })
50
+ .send();
51
+ if (!value) return null;
52
+ owned(value.owner, config.programId, address);
53
+ const data = bytes(value.data[0]);
54
+ return { address, lamports: BigInt(value.lamports), data, collection: decodeCollection(data) };
55
+ }
56
+
57
+ export async function fetchHeader(
58
+ rpc: Rpc<GetAccountInfoApi>,
59
+ address: Address,
60
+ config: FetchConfig = {},
61
+ ): Promise<CollectionHeader | null> {
62
+ const { value } = await rpc
63
+ .getAccountInfo(address, {
64
+ encoding: "base64",
65
+ commitment: config.commitment ?? "confirmed",
66
+ dataSlice: { offset: 0, length: HEADER_LEN },
67
+ })
68
+ .send();
69
+ if (!value) return null;
70
+ owned(value.owner, config.programId, address);
71
+ return decodeHeader(bytes(value.data[0]));
72
+ }
73
+
74
+ export async function fetchSlot(
75
+ rpc: Rpc<GetAccountInfoApi>,
76
+ address: Address,
77
+ id: number,
78
+ config: FetchConfig = {},
79
+ ): Promise<Slot | null> {
80
+ const { value } = await rpc
81
+ .getAccountInfo(address, {
82
+ encoding: "base64",
83
+ commitment: config.commitment ?? "confirmed",
84
+ dataSlice: { offset: slotOffset(id), length: SLOT_LEN },
85
+ })
86
+ .send();
87
+ if (!value) return null;
88
+ owned(value.owner, config.programId, address);
89
+ const data = bytes(value.data[0]);
90
+ return data.length < SLOT_LEN ? null : decodeSlot(data, id);
91
+ }
92
+
93
+ export interface FoundCollection {
94
+ address: Address;
95
+ header: CollectionHeader;
96
+ }
97
+
98
+ export async function findCollections(
99
+ rpc: Rpc<GetProgramAccountsApi>,
100
+ programId: Address,
101
+ options: { creator?: Address; commitment?: Commitment } = {},
102
+ ): Promise<FoundCollection[]> {
103
+ const tagAndVersion = getBase58Decoder().decode(new Uint8Array([COLLECTION_TAG, LAYOUT_VERSION])) as Base58EncodedBytes;
104
+ const filters = [
105
+ { memcmp: { offset: 0n, bytes: tagAndVersion, encoding: "base58" as const } },
106
+ ...(options.creator
107
+ ? [{ memcmp: { offset: BigInt(HEADER_OFFSET.creator), bytes: options.creator as string as Base58EncodedBytes, encoding: "base58" as const } }]
108
+ : []),
109
+ ];
110
+ const accounts = (await rpc
111
+ .getProgramAccounts(programId, {
112
+ encoding: "base64",
113
+ commitment: options.commitment ?? "confirmed",
114
+ dataSlice: { offset: 0, length: HEADER_LEN },
115
+ filters,
116
+ })
117
+ .send()) as unknown as readonly { pubkey: Address; account: { data: readonly [string, string] } }[];
118
+ return accounts.map(({ pubkey, account }) => ({ address: pubkey, header: decodeHeader(bytes(account.data[0])) }));
119
+ }
120
+
121
+ export async function getLamportsPerByte(rpc: Rpc<GetMinimumBalanceForRentExemptionApi>): Promise<bigint> {
122
+ const min = await rpc.getMinimumBalanceForRentExemption(0n).send();
123
+ return BigInt(min) / BigInt(ACCOUNT_OVERHEAD);
124
+ }
125
+
126
+ export const rentExemptMinimum = (dataLength: number, lamportsPerByte: bigint): bigint =>
127
+ BigInt(ACCOUNT_OVERHEAD + dataLength) * lamportsPerByte;
package/src/seed.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { getAddressEncoder, type Address } from "@solana/kit";
2
+ import { MAX_SUPPLY } from "./constants.ts";
3
+
4
+ async function sha256(...parts: Uint8Array[]): Promise<Uint8Array> {
5
+ const buf = new Uint8Array(new ArrayBuffer(parts.reduce((n, p) => n + p.length, 0)));
6
+ let off = 0;
7
+ for (const p of parts) {
8
+ buf.set(p, off);
9
+ off += p.length;
10
+ }
11
+ return new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", buf));
12
+ }
13
+
14
+ export interface TokenSeedArgs {
15
+ domain: string;
16
+ collection: Address;
17
+ entropy: Uint8Array;
18
+ id: number;
19
+ }
20
+
21
+ export function tokenSeed({ domain, collection, entropy, id }: TokenSeedArgs): Promise<Uint8Array> {
22
+ if (!Number.isInteger(id) || id < 0 || id >= MAX_SUPPLY) throw new RangeError(`id must be a u16 below ${MAX_SUPPLY}, got ${id}`);
23
+ const idLe = new Uint8Array([id & 0xff, id >> 8]);
24
+ return sha256(
25
+ new TextEncoder().encode(domain),
26
+ new Uint8Array(getAddressEncoder().encode(collection)),
27
+ entropy,
28
+ idLe,
29
+ );
30
+ }