|
1 | 1 | import 'dotenv/config'; |
2 | | -import { Client, PrivateKey, KeyList } from '@hashgraph/sdk'; |
| 2 | +import { |
| 3 | + Client, |
| 4 | + PrivateKey, |
| 5 | + KeyList, |
| 6 | + AccountCreateTransaction, |
| 7 | + Hbar, |
| 8 | +} from '@hashgraph/sdk'; |
3 | 9 | import { HCS16Client } from '../../src/hcs-16/sdk'; |
| 10 | +import { HCS11Client } from '../../src/hcs-11/client'; |
4 | 11 | import { HCS16BaseClient } from '../../src/hcs-16/base-client'; |
5 | 12 | import { FloraTopicType } from '../../src/hcs-16/types'; |
6 | 13 |
|
7 | 14 | async function main() { |
8 | | - const network = (process.env.HEDERA_NETWORK as 'mainnet' | 'testnet') || 'testnet'; |
9 | | - const operatorId = process.env.HEDERA_OPERATOR_ID || process.env.HEDERA_ACCOUNT_ID; |
10 | | - const operatorKey = process.env.HEDERA_OPERATOR_KEY || process.env.HEDERA_PRIVATE_KEY; |
| 15 | + const network = |
| 16 | + (process.env.HEDERA_NETWORK as 'mainnet' | 'testnet') || 'testnet'; |
| 17 | + const operatorId = |
| 18 | + process.env.HEDERA_OPERATOR_ID || process.env.HEDERA_ACCOUNT_ID; |
| 19 | + const operatorKey = |
| 20 | + process.env.HEDERA_OPERATOR_KEY || process.env.HEDERA_PRIVATE_KEY; |
11 | 21 | if (!operatorId || !operatorKey) { |
12 | | - console.error('Missing HEDERA_OPERATOR_ID/HEDERA_OPERATOR_KEY in environment'); |
| 22 | + console.error( |
| 23 | + 'Missing HEDERA_OPERATOR_ID/HEDERA_OPERATOR_KEY in environment', |
| 24 | + ); |
13 | 25 | process.exit(1); |
14 | 26 | } |
15 | 27 |
|
16 | 28 | const client = new HCS16Client({ network, operatorId, operatorKey }); |
| 29 | + const hcs11 = new HCS11Client({ |
| 30 | + network, |
| 31 | + auth: { operatorId, privateKey: operatorKey } as any, |
| 32 | + } as any); |
17 | 33 |
|
| 34 | + console.log('1) 🔐 Creating Flora member keys (2-of-3 threshold)'); |
| 35 | + const threshold = 2; |
18 | 36 | const k1 = PrivateKey.generateECDSA(); |
19 | 37 | const k2 = PrivateKey.generateECDSA(); |
20 | 38 | const k3 = PrivateKey.generateECDSA(); |
21 | | - const keyList = new KeyList([k1.publicKey, k2.publicKey, k3.publicKey], 2); |
| 39 | + const memberKeys = [k1, k2, k3]; |
22 | 40 |
|
23 | | - const payerClient = network === 'mainnet' ? Client.forMainnet() : Client.forTestnet(); |
| 41 | + const payerClient = |
| 42 | + network === 'mainnet' ? Client.forMainnet() : Client.forTestnet(); |
24 | 43 | payerClient.setOperator(operatorId, operatorKey); |
| 44 | + console.log(' ↳ 🧾 Creating member accounts'); |
| 45 | + const memberAccounts: string[] = []; |
| 46 | + for (const [index, key] of memberKeys.entries()) { |
| 47 | + const resp = await new AccountCreateTransaction() |
| 48 | + .setKey(key.publicKey) |
| 49 | + .setInitialBalance(new Hbar(5)) |
| 50 | + .execute(payerClient); |
| 51 | + const receipt = await resp.getReceipt(payerClient); |
| 52 | + if (!receipt.accountId) { |
| 53 | + throw new Error(`Failed to create Flora member account ${index + 1}`); |
| 54 | + } |
| 55 | + const accountId = receipt.accountId.toString(); |
| 56 | + memberAccounts.push(accountId); |
| 57 | + console.log(` • Member ${index + 1}: ${accountId}`); |
| 58 | + } |
| 59 | + |
| 60 | + const keyList = new KeyList( |
| 61 | + memberKeys.map(key => key.publicKey), |
| 62 | + threshold, |
| 63 | + ); |
| 64 | + const submitKeyList = new KeyList( |
| 65 | + memberKeys.map(key => key.publicKey), |
| 66 | + 1, |
| 67 | + ); |
| 68 | + |
25 | 69 | const { buildHcs16CreateAccountTx } = await import('../../src/hcs-16/tx'); |
26 | | - const accountTx = buildHcs16CreateAccountTx({ keyList, initialBalanceHbar: 2, maxAutomaticTokenAssociations: -1 }); |
| 70 | + const accountTx = buildHcs16CreateAccountTx({ |
| 71 | + keyList, |
| 72 | + initialBalanceHbar: 2, |
| 73 | + maxAutomaticTokenAssociations: -1, |
| 74 | + }); |
| 75 | + console.log(' ↳ 🧾 Submitting AccountCreateTransaction'); |
27 | 76 | const accountResp = await accountTx.execute(payerClient); |
28 | 77 | const accountReceipt = await accountResp.getReceipt(payerClient); |
29 | 78 | if (!accountReceipt.accountId) { |
30 | 79 | throw new Error('Failed to create Flora account'); |
31 | 80 | } |
32 | 81 | const floraAccountId = accountReceipt.accountId.toString(); |
33 | | - console.log('Flora account created:', floraAccountId); |
| 82 | + console.log(' ✅ Flora account created:', floraAccountId); |
34 | 83 |
|
35 | | - const comm = await client.createFloraTopic({ floraAccountId, topicType: FloraTopicType.COMMUNICATION }); |
36 | | - const tx = await client.createFloraTopic({ floraAccountId, topicType: FloraTopicType.TRANSACTION }); |
37 | | - const state = await client.createFloraTopic({ floraAccountId, topicType: FloraTopicType.STATE }); |
38 | | - console.log('Flora topics:', { communication: comm, transaction: tx, state }); |
| 84 | + console.log('2) 🧵 Creating Flora topics (CTopic/TTopic/STopic)'); |
| 85 | + const comm = await client.createFloraTopic({ |
| 86 | + floraAccountId, |
| 87 | + topicType: FloraTopicType.COMMUNICATION, |
| 88 | + adminKey: keyList, |
| 89 | + submitKey: submitKeyList, |
| 90 | + signerKeys: memberKeys.slice(0, threshold), |
| 91 | + }); |
| 92 | + const tx = await client.createFloraTopic({ |
| 93 | + floraAccountId, |
| 94 | + topicType: FloraTopicType.TRANSACTION, |
| 95 | + adminKey: keyList, |
| 96 | + submitKey: submitKeyList, |
| 97 | + signerKeys: memberKeys.slice(0, threshold), |
| 98 | + }); |
| 99 | + const state = await client.createFloraTopic({ |
| 100 | + floraAccountId, |
| 101 | + topicType: FloraTopicType.STATE, |
| 102 | + adminKey: keyList, |
| 103 | + submitKey: submitKeyList, |
| 104 | + signerKeys: memberKeys.slice(0, threshold), |
| 105 | + }); |
| 106 | + console.log(' ✅ Topics ready:', { |
| 107 | + communication: comm, |
| 108 | + transaction: tx, |
| 109 | + state, |
| 110 | + }); |
| 111 | + |
| 112 | + // Publish minimal Flora profile and update Flora memo (hcs-11:<resource>) |
| 113 | + const profile: any = { |
| 114 | + version: '1.0', |
| 115 | + type: 3, |
| 116 | + display_name: 'Example Flora', |
| 117 | + // Minimal valid profile per HCS-11 Flora schema |
| 118 | + members: memberAccounts.map(accountId => ({ accountId })), |
| 119 | + threshold, |
| 120 | + topics: { communication: comm, transaction: tx, state }, |
| 121 | + inboundTopicId: comm, |
| 122 | + outboundTopicId: tx, |
| 123 | + }; |
| 124 | + try { |
| 125 | + console.log('3) 🗂️ Publishing HCS-11 Flora profile and updating memo'); |
| 126 | + const { profileResource } = await client.publishFloraProfileAndMemo({ |
| 127 | + hcs11, |
| 128 | + floraAccountId, |
| 129 | + profile, |
| 130 | + }); |
| 131 | + console.log(' ✅ Profile published:', profileResource); |
| 132 | + } catch (e) { |
| 133 | + console.warn( |
| 134 | + ' ⚠️ Skipping profile inscription in demo environment:', |
| 135 | + (e as Error).message, |
| 136 | + ); |
| 137 | + console.warn( |
| 138 | + ' ℹ️ Ensure registrar availability and correct operator signatures to enable inscription.', |
| 139 | + ); |
| 140 | + } |
| 141 | + |
| 142 | + const signingMemberAccount = memberAccounts[0]; |
| 143 | + const signingKey = memberKeys[0]; |
39 | 144 |
|
| 145 | + console.log('4) 📣 Publishing flora_created (CTopic)'); |
40 | 146 | const receipt = await client.sendFloraCreated({ |
41 | 147 | topicId: comm, |
42 | | - operatorId: `${operatorId}@${floraAccountId}`, |
| 148 | + operatorId: `${signingMemberAccount}@${floraAccountId}`, |
43 | 149 | floraAccountId, |
44 | 150 | topics: { communication: comm, transaction: tx, state }, |
| 151 | + signerKey: signingKey, |
45 | 152 | }); |
46 | | - console.log('flora_created receipt status:', receipt.status.toString()); |
| 153 | + console.log(' ✅ flora_created status:', receipt.status.toString()); |
47 | 154 | const stateHash = '0x' + Date.now().toString(16); |
48 | | - await client.sendStateUpdate({ topicId: state, operatorId: `${operatorId}@${floraAccountId}`, hash: stateHash }); |
49 | | - console.log('state_update sent with hash:', stateHash); |
| 155 | + console.log('5) 🧩 Publishing state_update (STopic)'); |
| 156 | + await client.sendStateUpdate({ |
| 157 | + topicId: state, |
| 158 | + operatorId: `${signingMemberAccount}@${floraAccountId}`, |
| 159 | + hash: stateHash, |
| 160 | + signerKey: signingKey, |
| 161 | + }); |
| 162 | + console.log(' ✅ state_update hash:', stateHash); |
| 163 | + console.log('6) 🧾 Publishing state_hash (HCS-17 on STopic)'); |
| 164 | + await client.sendStateHash({ |
| 165 | + topicId: state, |
| 166 | + stateHash, |
| 167 | + accountId: floraAccountId, |
| 168 | + topics: [comm, tx, state], |
| 169 | + signerKey: signingKey, |
| 170 | + }); |
| 171 | + console.log(' ✅ state_hash committed via HCS-17'); |
50 | 172 |
|
51 | 173 | const helper = new HCS16BaseClient({ network }); |
52 | 174 | const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); |
53 | 175 | let printed = false; |
54 | | - for (let i = 0; i < 5; i++) { |
| 176 | + console.log('7) 🔎 Reading back latest messages'); |
| 177 | + for (let i = 0; i < 6; i++) { |
55 | 178 | try { |
56 | | - const commMsgs = await helper.getRecentMessages(comm, { limit: 1, order: 'desc' }); |
57 | | - const stateMsgs = await helper.getRecentMessages(state, { limit: 1, order: 'desc' }); |
| 179 | + const commMsgs = await helper.getRecentMessages(comm, { |
| 180 | + limit: 1, |
| 181 | + order: 'desc', |
| 182 | + }); |
| 183 | + const stateMsgs = await helper.getRecentMessages(state, { |
| 184 | + limit: 1, |
| 185 | + order: 'desc', |
| 186 | + }); |
58 | 187 | if (commMsgs.length > 0) { |
59 | | - console.log('Latest flora_created message:', commMsgs[0]); |
| 188 | + console.log(' 📨 Latest flora_created message:', commMsgs[0]); |
60 | 189 | printed = true; |
61 | 190 | } |
62 | 191 | if (stateMsgs.length > 0) { |
63 | | - console.log('Latest state_update message:', stateMsgs[0]); |
| 192 | + console.log(' 📨 Latest state_update message:', stateMsgs[0]); |
64 | 193 | printed = true; |
65 | 194 | } |
66 | 195 | if (printed) { |
|
0 commit comments