diff --git a/demo/hcs-16/create-flora-demo.ts b/demo/hcs-16/create-flora-demo.ts index abb9396f..ceea973d 100644 --- a/demo/hcs-16/create-flora-demo.ts +++ b/demo/hcs-16/create-flora-demo.ts @@ -1,66 +1,195 @@ import 'dotenv/config'; -import { Client, PrivateKey, KeyList } from '@hashgraph/sdk'; +import { + Client, + PrivateKey, + KeyList, + AccountCreateTransaction, + Hbar, +} from '@hashgraph/sdk'; import { HCS16Client } from '../../src/hcs-16/sdk'; +import { HCS11Client } from '../../src/hcs-11/client'; import { HCS16BaseClient } from '../../src/hcs-16/base-client'; import { FloraTopicType } from '../../src/hcs-16/types'; async function main() { - const network = (process.env.HEDERA_NETWORK as 'mainnet' | 'testnet') || 'testnet'; - const operatorId = process.env.HEDERA_OPERATOR_ID || process.env.HEDERA_ACCOUNT_ID; - const operatorKey = process.env.HEDERA_OPERATOR_KEY || process.env.HEDERA_PRIVATE_KEY; + const network = + (process.env.HEDERA_NETWORK as 'mainnet' | 'testnet') || 'testnet'; + const operatorId = + process.env.HEDERA_OPERATOR_ID || process.env.HEDERA_ACCOUNT_ID; + const operatorKey = + process.env.HEDERA_OPERATOR_KEY || process.env.HEDERA_PRIVATE_KEY; if (!operatorId || !operatorKey) { - console.error('Missing HEDERA_OPERATOR_ID/HEDERA_OPERATOR_KEY in environment'); + console.error( + 'Missing HEDERA_OPERATOR_ID/HEDERA_OPERATOR_KEY in environment', + ); process.exit(1); } const client = new HCS16Client({ network, operatorId, operatorKey }); + const hcs11 = new HCS11Client({ + network, + auth: { operatorId, privateKey: operatorKey } as any, + } as any); + console.log('1) 🔐 Creating Flora member keys (2-of-3 threshold)'); + const threshold = 2; const k1 = PrivateKey.generateECDSA(); const k2 = PrivateKey.generateECDSA(); const k3 = PrivateKey.generateECDSA(); - const keyList = new KeyList([k1.publicKey, k2.publicKey, k3.publicKey], 2); + const memberKeys = [k1, k2, k3]; - const payerClient = network === 'mainnet' ? Client.forMainnet() : Client.forTestnet(); + const payerClient = + network === 'mainnet' ? Client.forMainnet() : Client.forTestnet(); payerClient.setOperator(operatorId, operatorKey); + console.log(' ↳ 🧾 Creating member accounts'); + const memberAccounts: string[] = []; + for (const [index, key] of memberKeys.entries()) { + const resp = await new AccountCreateTransaction() + .setKey(key.publicKey) + .setInitialBalance(new Hbar(5)) + .execute(payerClient); + const receipt = await resp.getReceipt(payerClient); + if (!receipt.accountId) { + throw new Error(`Failed to create Flora member account ${index + 1}`); + } + const accountId = receipt.accountId.toString(); + memberAccounts.push(accountId); + console.log(` • Member ${index + 1}: ${accountId}`); + } + + const keyList = new KeyList( + memberKeys.map(key => key.publicKey), + threshold, + ); + const submitKeyList = new KeyList( + memberKeys.map(key => key.publicKey), + 1, + ); + const { buildHcs16CreateAccountTx } = await import('../../src/hcs-16/tx'); - const accountTx = buildHcs16CreateAccountTx({ keyList, initialBalanceHbar: 2, maxAutomaticTokenAssociations: -1 }); + const accountTx = buildHcs16CreateAccountTx({ + keyList, + initialBalanceHbar: 2, + maxAutomaticTokenAssociations: -1, + }); + console.log(' ↳ 🧾 Submitting AccountCreateTransaction'); const accountResp = await accountTx.execute(payerClient); const accountReceipt = await accountResp.getReceipt(payerClient); if (!accountReceipt.accountId) { throw new Error('Failed to create Flora account'); } const floraAccountId = accountReceipt.accountId.toString(); - console.log('Flora account created:', floraAccountId); + console.log(' ✅ Flora account created:', floraAccountId); - const comm = await client.createFloraTopic({ floraAccountId, topicType: FloraTopicType.COMMUNICATION }); - const tx = await client.createFloraTopic({ floraAccountId, topicType: FloraTopicType.TRANSACTION }); - const state = await client.createFloraTopic({ floraAccountId, topicType: FloraTopicType.STATE }); - console.log('Flora topics:', { communication: comm, transaction: tx, state }); + console.log('2) 🧵 Creating Flora topics (CTopic/TTopic/STopic)'); + const comm = await client.createFloraTopic({ + floraAccountId, + topicType: FloraTopicType.COMMUNICATION, + adminKey: keyList, + submitKey: submitKeyList, + signerKeys: memberKeys.slice(0, threshold), + }); + const tx = await client.createFloraTopic({ + floraAccountId, + topicType: FloraTopicType.TRANSACTION, + adminKey: keyList, + submitKey: submitKeyList, + signerKeys: memberKeys.slice(0, threshold), + }); + const state = await client.createFloraTopic({ + floraAccountId, + topicType: FloraTopicType.STATE, + adminKey: keyList, + submitKey: submitKeyList, + signerKeys: memberKeys.slice(0, threshold), + }); + console.log(' ✅ Topics ready:', { + communication: comm, + transaction: tx, + state, + }); + + // Publish minimal Flora profile and update Flora memo (hcs-11:) + const profile: any = { + version: '1.0', + type: 3, + display_name: 'Example Flora', + // Minimal valid profile per HCS-11 Flora schema + members: memberAccounts.map(accountId => ({ accountId })), + threshold, + topics: { communication: comm, transaction: tx, state }, + inboundTopicId: comm, + outboundTopicId: tx, + }; + try { + console.log('3) 🗂️ Publishing HCS-11 Flora profile and updating memo'); + const { profileResource } = await client.publishFloraProfileAndMemo({ + hcs11, + floraAccountId, + profile, + }); + console.log(' ✅ Profile published:', profileResource); + } catch (e) { + console.warn( + ' ⚠️ Skipping profile inscription in demo environment:', + (e as Error).message, + ); + console.warn( + ' ℹ️ Ensure registrar availability and correct operator signatures to enable inscription.', + ); + } + + const signingMemberAccount = memberAccounts[0]; + const signingKey = memberKeys[0]; + console.log('4) 📣 Publishing flora_created (CTopic)'); const receipt = await client.sendFloraCreated({ topicId: comm, - operatorId: `${operatorId}@${floraAccountId}`, + operatorId: `${signingMemberAccount}@${floraAccountId}`, floraAccountId, topics: { communication: comm, transaction: tx, state }, + signerKey: signingKey, }); - console.log('flora_created receipt status:', receipt.status.toString()); + console.log(' ✅ flora_created status:', receipt.status.toString()); const stateHash = '0x' + Date.now().toString(16); - await client.sendStateUpdate({ topicId: state, operatorId: `${operatorId}@${floraAccountId}`, hash: stateHash }); - console.log('state_update sent with hash:', stateHash); + console.log('5) 🧩 Publishing state_update (STopic)'); + await client.sendStateUpdate({ + topicId: state, + operatorId: `${signingMemberAccount}@${floraAccountId}`, + hash: stateHash, + signerKey: signingKey, + }); + console.log(' ✅ state_update hash:', stateHash); + console.log('6) 🧾 Publishing state_hash (HCS-17 on STopic)'); + await client.sendStateHash({ + topicId: state, + stateHash, + accountId: floraAccountId, + topics: [comm, tx, state], + signerKey: signingKey, + }); + console.log(' ✅ state_hash committed via HCS-17'); const helper = new HCS16BaseClient({ network }); const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); let printed = false; - for (let i = 0; i < 5; i++) { + console.log('7) 🔎 Reading back latest messages'); + for (let i = 0; i < 6; i++) { try { - const commMsgs = await helper.getRecentMessages(comm, { limit: 1, order: 'desc' }); - const stateMsgs = await helper.getRecentMessages(state, { limit: 1, order: 'desc' }); + const commMsgs = await helper.getRecentMessages(comm, { + limit: 1, + order: 'desc', + }); + const stateMsgs = await helper.getRecentMessages(state, { + limit: 1, + order: 'desc', + }); if (commMsgs.length > 0) { - console.log('Latest flora_created message:', commMsgs[0]); + console.log(' 📨 Latest flora_created message:', commMsgs[0]); printed = true; } if (stateMsgs.length > 0) { - console.log('Latest state_update message:', stateMsgs[0]); + console.log(' 📨 Latest state_update message:', stateMsgs[0]); printed = true; } if (printed) { diff --git a/demo/hcs-16/flora-e2e-demo.ts b/demo/hcs-16/flora-e2e-demo.ts index d6db8098..0ea097c9 100644 --- a/demo/hcs-16/flora-e2e-demo.ts +++ b/demo/hcs-16/flora-e2e-demo.ts @@ -163,19 +163,24 @@ async function main() { const h16 = new HCS16Client({ network, operatorId, operatorKey }); const h16Base = new HCS16BaseClient({ network }); - const { petalA, petalB, petalC } = await seq.run('Create Petal A/B/C accounts (HCS-15)', async () => { - const pa = await createPetal(h15, h10); - const pb = await createPetal(h15, h10); - const pc = await createPetal(h15, h10); - return { petalA: pa, petalB: pb, petalC: pc }; - }); + const { petalA, petalB, petalC } = await seq.run( + 'Create Petal A/B/C accounts (HCS-15)', + async () => { + const pa = await createPetal(h15, h10); + const pb = await createPetal(h15, h10); + const pc = await createPetal(h15, h10); + return { petalA: pa, petalB: pb, petalC: pc }; + }, + ); const discovery = new HCS18Client({ network: network as NetworkType, operatorId, operatorKey, }); - const createdDiscovery = await discovery.createDiscoveryTopic({ ttlSeconds: 300 }); + const createdDiscovery = await discovery.createDiscoveryTopic({ + ttlSeconds: 300, + }); const discoveryTopicId = createdDiscovery.topicId; const dcA = new HCS18Client({ @@ -195,31 +200,93 @@ async function main() { }); await seq.run('Announce A on discovery (HCS-18)', async () => { - await dcA.announce({ discoveryTopicId, data: { account: petalA.petalAccountId, petal: { name: 'A', priority: 600 }, capabilities: { protocols: ['hcs-16', 'hcs-17', 'hcs-18'] }, valid_for: 10000 } }); + await dcA.announce({ + discoveryTopicId, + data: { + account: petalA.petalAccountId, + petal: { name: 'A', priority: 600 }, + capabilities: { protocols: ['hcs-16', 'hcs-17', 'hcs-18'] }, + valid_for: 10000, + }, + }); }); await seq.run('Announce B on discovery (HCS-18)', async () => { - await dcB.announce({ discoveryTopicId, data: { account: petalB.petalAccountId, petal: { name: 'B', priority: 500 }, capabilities: { protocols: ['hcs-16', 'hcs-17', 'hcs-18'] }, valid_for: 10000 } }); + await dcB.announce({ + discoveryTopicId, + data: { + account: petalB.petalAccountId, + petal: { name: 'B', priority: 500 }, + capabilities: { protocols: ['hcs-16', 'hcs-17', 'hcs-18'] }, + valid_for: 10000, + }, + }); }); await seq.run('Announce C on discovery (HCS-18)', async () => { - await dcC.announce({ discoveryTopicId, data: { account: petalC.petalAccountId, petal: { name: 'C', priority: 500 }, capabilities: { protocols: ['hcs-16', 'hcs-17', 'hcs-18'] }, valid_for: 10000 } }); + await dcC.announce({ + discoveryTopicId, + data: { + account: petalC.petalAccountId, + petal: { name: 'C', priority: 500 }, + capabilities: { protocols: ['hcs-16', 'hcs-17', 'hcs-18'] }, + valid_for: 10000, + }, + }); }); - const { sequenceNumber: proposalSeq } = await seq.run('Propose Flora formation (HCS-18)', async () => { - const r = await dcA.propose({ discoveryTopicId, data: { proposer: petalA.petalAccountId, members: [ { account: petalA.petalAccountId, priority: 600 }, { account: petalB.petalAccountId, priority: 500 }, { account: petalC.petalAccountId, priority: 500 } ], config: { name: 'Flora E2E', threshold: 2, purpose: 'E2E' } } }); - return r; - }); + const { sequenceNumber: proposalSeq } = await seq.run( + 'Propose Flora formation (HCS-18)', + async () => { + const r = await dcA.propose({ + discoveryTopicId, + data: { + proposer: petalA.petalAccountId, + members: [ + { account: petalA.petalAccountId, priority: 600 }, + { account: petalB.petalAccountId, priority: 500 }, + { account: petalC.petalAccountId, priority: 500 }, + ], + config: { name: 'Flora E2E', threshold: 2, purpose: 'E2E' }, + }, + }); + return r; + }, + ); await seq.run('Respond B accept (HCS-18)', async () => { - await dcB.respond({ discoveryTopicId, data: { responder: petalB.petalAccountId, proposal_seq: proposalSeq, decision: 'accept' } }); + await dcB.respond({ + discoveryTopicId, + data: { + responder: petalB.petalAccountId, + proposal_seq: proposalSeq, + decision: 'accept', + }, + }); }); await seq.run('Respond C accept (HCS-18)', async () => { - await dcC.respond({ discoveryTopicId, data: { responder: petalC.petalAccountId, proposal_seq: proposalSeq, decision: 'accept' } }); + await dcC.respond({ + discoveryTopicId, + data: { + responder: petalC.petalAccountId, + proposal_seq: proposalSeq, + decision: 'accept', + }, + }); }); - const keyList = await seq.run('Create Flora account (2-of-3 keylist)', async () => { - const k = await h16.assembleKeyList({ members: [petalA.petalAccountId, petalB.petalAccountId, petalC.petalAccountId], threshold: 2 }); - return k; - }); + const keyList = await seq.run( + 'Create Flora account (2-of-3 keylist)', + async () => { + const k = await h16.assembleKeyList({ + members: [ + petalA.petalAccountId, + petalB.petalAccountId, + petalC.petalAccountId, + ], + threshold: 2, + }); + return k; + }, + ); const submitList = new KeyList([], 1) as any; for (const p of [petalA, petalB, petalC]) { const pub = await h16.mirrorNode.getPublicKey(p.petalAccountId); @@ -239,7 +306,9 @@ async function main() { const floraAccountId = accRec.accountId.toString(); console.log('Flora account:', floraAccountId); - await seq.run('Create C/T/S topics', async () => { return; }); + await seq.run('Create C/T/S topics', async () => { + return; + }); const commTx = buildHcs16CreateFloraTopicTx({ floraAccountId, topicType: 0 as any, @@ -318,10 +387,18 @@ async function main() { return; }); - const scheduleId = await seq.run('Schedule transfer 1 HBAR to 0.0.800', async () => { - const id = await scheduleTransferFromFlora(nodeClient, floraAccountId, '0.0.800', 1); - return id || ''; - }); + const scheduleId = await seq.run( + 'Schedule transfer 1 HBAR to 0.0.800', + async () => { + const id = await scheduleTransferFromFlora( + nodeClient, + floraAccountId, + '0.0.800', + 1, + ); + return id || ''; + }, + ); if (scheduleId) { await seq.run('Post transaction with schedule_id (TTopic)', async () => { const { buildHcs16TransactionTx } = await import('../../src/hcs-16/tx'); @@ -349,31 +426,42 @@ async function main() { } console.log('Creating Petal D and issuing flora_join_request...'); - const petalD = await seq.run('Create Petal D and post flora_join_request', async () => { - const pd = await createPetal(h15, h10); - const tx = buildHcs16FloraJoinRequestTx({ - topicId: topics.communication, - operatorId: pd.petalAccountId, - candidateAccountId: pd.petalAccountId, - }); - const frozen = await tx.freezeWith(nodeClient); - const signed = await frozen.sign(petalA.baseKey); - await (await signed.execute(nodeClient)).getReceipt(nodeClient); - return pd; - }); + const petalD = await seq.run( + 'Create Petal D and post flora_join_request', + async () => { + const pd = await createPetal(h15, h10); + const tx = buildHcs16FloraJoinRequestTx({ + topicId: topics.communication, + operatorId: pd.petalAccountId, + accountId: pd.petalAccountId, + connectionRequestId: 51234, + connectionTopicId: '0.0.conn', + connectionSeq: 27, + memo: 'This account reached out and requested to join', + }); + const frozen = await tx.freezeWith(nodeClient); + const signed = await frozen.sign(petalA.baseKey); + await (await signed.execute(nodeClient)).getReceipt(nodeClient); + return pd; + }, + ); await seq.run('Members A/B post flora_join_vote approvals', async () => { const v1 = buildHcs16FloraJoinVoteTx({ topicId: topics.communication, operatorId: `${petalA.petalAccountId}@${floraAccountId}`, - candidateAccountId: petalD.petalAccountId, + accountId: petalD.petalAccountId, approve: true, + connectionRequestId: 51234, + connectionSeq: 27, }); const v2 = buildHcs16FloraJoinVoteTx({ topicId: topics.communication, operatorId: `${petalB.petalAccountId}@${floraAccountId}`, - candidateAccountId: petalD.petalAccountId, + accountId: petalD.petalAccountId, approve: true, + connectionRequestId: 51234, + connectionSeq: 27, }); const f1 = await v1.freezeWith(nodeClient); const s1 = await f1.sign(petalA.baseKey); @@ -395,6 +483,7 @@ async function main() { petalD.petalAccountId, ], epoch: 2, + memo: 'Membership updated to include D', }); const f = await acc.freezeWith(nodeClient); const s = await f.sign(petalA.baseKey); @@ -403,8 +492,14 @@ async function main() { }); await seq.run('Mirror readback latest messages', async () => { - const latestCreated = await h16Base.getLatestMessage(topics.communication, 'flora_created'); - const latestState = await h16Base.getLatestMessage(topics.state, 'state_update'); + const latestCreated = await h16Base.getLatestMessage( + topics.communication, + 'flora_created', + ); + const latestState = await h16Base.getLatestMessage( + topics.state, + 'state_update', + ); log.info('Latest flora_created fetched'); log.info('Latest state_update fetched'); return { latestCreated, latestState }; diff --git a/src/common/tx/tx-utils.ts b/src/common/tx/tx-utils.ts index 8a33b473..8cde97e3 100644 --- a/src/common/tx/tx-utils.ts +++ b/src/common/tx/tx-utils.ts @@ -62,7 +62,10 @@ export function buildMessageTx(params: { .setTopicId(TopicId.fromString(params.topicId)) .setMessage(params.message); if (params.transactionMemo) { - tx.setTransactionMemo(params.transactionMemo); + // setTransactionMemo may not exist on all installed SDK versions + if (typeof (tx as any).setTransactionMemo === 'function') { + (tx as any).setTransactionMemo(params.transactionMemo); + } } return tx; } diff --git a/src/hcs-16/base-client.ts b/src/hcs-16/base-client.ts index ef9fdb7a..bace97fb 100644 --- a/src/hcs-16/base-client.ts +++ b/src/hcs-16/base-client.ts @@ -1,5 +1,6 @@ import { HederaMirrorNode } from '../services/mirror-node'; import { KeyList, TopicCreateTransaction, PublicKey } from '@hashgraph/sdk'; +import { KeyTypeDetector, KeyType } from '../utils/key-type-detector'; import { buildHcs16CreateFloraTopicTx } from './tx'; import { Logger, ILogger } from '../utils/logger'; import { NetworkType } from '../utils/types'; @@ -15,18 +16,33 @@ export class HCS16BaseClient { public mirrorNode: HederaMirrorNode; protected readonly logger: ILogger; - constructor(params: { network: NetworkType; logger?: ILogger; mirrorNodeUrl?: string }) { + constructor(params: { + network: NetworkType; + logger?: ILogger; + mirrorNodeUrl?: string; + }) { this.network = params.network; - this.logger = params.logger || new Logger({ level: 'info', module: 'HCS-16' }); + this.logger = + params.logger || new Logger({ level: 'info', module: 'HCS-16' }); this.mirrorNode = new HederaMirrorNode(this.network, this.logger, { customUrl: params.mirrorNodeUrl, }); } - async assembleKeyList(params: { members: string[]; threshold: number }): Promise { + async assembleKeyList(params: { + members: string[]; + threshold: number; + }): Promise { const keys: PublicKey[] = []; for (const accountId of params.members) { const pub = await this.mirrorNode.getPublicKey(accountId); + // Enforce ECDSA: ED25519 keys are not supported for Flora membership + const detected = KeyTypeDetector.detect(pub.toString()); + if (detected.type !== KeyType.ECDSA) { + throw new Error( + `HCS-16 requires ECDSA/secp256k1 keys for members. Account ${accountId} does not meet requirements.`, + ); + } keys.push(pub); } return new KeyList(keys, params.threshold); @@ -97,7 +113,11 @@ export class HCS16BaseClient { /** * Build a Flora message envelope by merging an operation body into the HCS‑16 envelope. */ - protected createFloraMessage(op: FloraOperation, operatorId: string, body?: Record): FloraMessage { + protected createFloraMessage( + op: FloraOperation, + operatorId: string, + body?: Record, + ): FloraMessage { const payload: FloraMessage = { p: 'hcs-16', op, @@ -112,7 +132,11 @@ export class HCS16BaseClient { */ async getRecentMessages( topicId: string, - options?: { limit?: number; order?: 'asc' | 'desc'; opFilter?: FloraOperation | string }, + options?: { + limit?: number; + order?: 'asc' | 'desc'; + opFilter?: FloraOperation | string; + }, ): Promise< Array<{ message: FloraMessage; @@ -123,10 +147,8 @@ export class HCS16BaseClient { > { const limit = options?.limit ?? 25; const order = options?.order ?? 'desc'; - const items: HCSMessageWithCommonFields[] = await this.mirrorNode.getTopicMessages( - topicId, - { limit, order }, - ); + const items: HCSMessageWithCommonFields[] = + await this.mirrorNode.getTopicMessages(topicId, { limit, order }); const results: Array<{ message: FloraMessage; @@ -177,11 +199,18 @@ export class HCS16BaseClient { /** * Return the latest valid HCS‑16 message on a topic, if any. */ - async getLatestMessage(topicId: string, opFilter?: FloraOperation | string): Promise< + async getLatestMessage( + topicId: string, + opFilter?: FloraOperation | string, + ): Promise< | (FloraMessage & { consensus_timestamp?: string; sequence_number: number }) | null > { - const items = await this.getRecentMessages(topicId, { limit: 1, order: 'desc', opFilter }); + const items = await this.getRecentMessages(topicId, { + limit: 1, + order: 'desc', + opFilter, + }); if (items.length === 0) { return null; } @@ -191,4 +220,23 @@ export class HCS16BaseClient { sequence_number: first.sequence_number, }); } + + /** Count flora_created acknowledgements on a communication topic. */ + async getActivationAcks(communicationTopicId: string): Promise { + const msgs = await this.getRecentMessages(communicationTopicId, { + limit: 100, + order: 'desc', + opFilter: 'flora_created', + }); + return msgs.length; + } + + /** Determine if Flora is activated by super-majority (≥T) acknowledgements. */ + async isFloraActivated(params: { + communicationTopicId: string; + threshold: number; + }): Promise { + const count = await this.getActivationAcks(params.communicationTopicId); + return count >= params.threshold; + } } diff --git a/src/hcs-16/browser.ts b/src/hcs-16/browser.ts index 0b1efdc4..4148989f 100644 --- a/src/hcs-16/browser.ts +++ b/src/hcs-16/browser.ts @@ -2,7 +2,20 @@ import type { HashinalsWalletConnectSDK } from '@hashgraphonline/hashinal-wc'; import type { DAppSigner } from '@hashgraph/hedera-wallet-connect'; import type { PublicKey, KeyList } from '@hashgraph/sdk'; import { ScheduleSignTransaction } from '@hashgraph/sdk'; -import { buildHcs16CreateFloraTopicTx, buildHcs16FloraCreatedTx, buildHcs16TransactionTx, buildHcs16StateUpdateTx, buildHcs16FloraJoinRequestTx, buildHcs16FloraJoinVoteTx, buildHcs16FloraJoinAcceptedTx, buildHcs16CreateAccountTx } from './tx'; +import { + buildHcs16CreateFloraTopicTx, + buildHcs16FloraCreatedTx, + buildHcs16TransactionTx, + buildHcs16StateUpdateTx, + buildHcs16StateHashTx, + buildHcs16FloraJoinRequestTx, + buildHcs16FloraJoinVoteTx, + buildHcs16FloraJoinAcceptedTx, + buildHcs16CreateAccountTx, + buildHcs16ScheduleAccountKeyUpdateTx, + buildHcs16ScheduleTopicKeyUpdateTx, + buildHcs16ScheduleAccountDeleteTx, +} from './tx'; import { FloraTopicType } from './types'; import { HCS16BaseClient } from './base-client'; @@ -26,7 +39,10 @@ export class HCS16BrowserClient extends HCS16BaseClient { } private ensureConnected(): string { - if (this.signer && typeof (this.signer as DAppSigner).getAccountId === 'function') { + if ( + this.signer && + typeof (this.signer as DAppSigner).getAccountId === 'function' + ) { return (this.signer as DAppSigner).getAccountId().toString(); } const info = this.hwc?.getAccountInfo?.(); @@ -37,6 +53,55 @@ export class HCS16BrowserClient extends HCS16BaseClient { return accountId; } + /** Create schedule to update Flora account KeyList (membership change) using wallet signer. */ + async scheduleAccountKeyUpdate(params: { + floraAccountId: string; + newKeyList: KeyList; + memo?: string; + }): Promise { + const signer = this.getSigner(); + const tx = buildHcs16ScheduleAccountKeyUpdateTx({ + floraAccountId: params.floraAccountId, + newKeyList: params.newKeyList, + memo: params.memo, + }); + const frozen = await tx.freezeWithSigner(signer); + await frozen.executeWithSigner(signer); + } + + /** Create schedule to update topic keys using wallet signer. */ + async scheduleTopicKeyUpdate(params: { + topicId: string; + adminKey?: PublicKey | KeyList; + submitKey?: PublicKey | KeyList; + memo?: string; + }): Promise { + const signer = this.getSigner(); + const tx = buildHcs16ScheduleTopicKeyUpdateTx({ + topicId: params.topicId, + adminKey: params.adminKey, + submitKey: params.submitKey, + memo: params.memo, + }); + const frozen = await tx.freezeWithSigner(signer); + await frozen.executeWithSigner(signer); + } + + /** Schedule Flora account deletion. */ + async scheduleFloraDeletion(params: { + floraAccountId: string; + transferAccountId: string; + memo?: string; + }): Promise { + const signer = this.getSigner(); + const tx = buildHcs16ScheduleAccountDeleteTx({ + floraAccountId: params.floraAccountId, + transferAccountId: params.transferAccountId, + memo: params.memo, + }); + const frozen = await tx.freezeWithSigner(signer); + await frozen.executeWithSigner(signer); + } private getSigner(): DAppSigner { if (this.signer) { return this.signer; @@ -101,7 +166,9 @@ export class HCS16BrowserClient extends HCS16BaseClient { */ async signSchedule(params: { scheduleId: string }): Promise { const signer = this.getSigner(); - const tx = await new ScheduleSignTransaction().setScheduleId(params.scheduleId).freezeWithSigner(signer); + const tx = await new ScheduleSignTransaction() + .setScheduleId(params.scheduleId) + .freezeWithSigner(signer); await tx.executeWithSigner(signer); } @@ -117,9 +184,20 @@ export class HCS16BrowserClient extends HCS16BaseClient { await frozen.executeWithSigner(signer); } - /** credit_purchase is not part of HCS-16 specification */ + async sendStateHash(params: { + topicId: string; + stateHash: string; + accountId: string; + topics: string[]; + memo?: string; + }): Promise { + const signer = this.getSigner(); + const tx = buildHcs16StateHashTx(params); + const frozen = await tx.freezeWithSigner(signer); + await frozen.executeWithSigner(signer); + } - + /** credit_purchase is not part of HCS-16 specification */ /** * Create Flora account and C/T/S topics using DAppSigner. @@ -137,12 +215,18 @@ export class HCS16BrowserClient extends HCS16BaseClient { topics: { communication: string; transaction: string; state: string }; }> { const signer = this.getSigner(); - const keyList = await this.assembleKeyList({ members: params.members, threshold: params.threshold }); + const keyList = await this.assembleKeyList({ + members: params.members, + threshold: params.threshold, + }); const submitList = await this.assembleSubmitKeyList(params.members); const createAcc = buildHcs16CreateAccountTx({ keyList, - initialBalanceHbar: typeof params.initialBalanceHbar === 'number' ? params.initialBalanceHbar : 5, + initialBalanceHbar: + typeof params.initialBalanceHbar === 'number' + ? params.initialBalanceHbar + : 5, maxAutomaticTokenAssociations: -1, }); const accFrozen = await createAcc.freezeWithSigner(signer); @@ -153,22 +237,26 @@ export class HCS16BrowserClient extends HCS16BaseClient { throw new Error('Failed to create Flora account'); } - const { communication: commTx, transaction: trnTx, state: stateTx } = this.buildFloraTopicCreateTxs({ + const { + communication: commTx, + transaction: trnTx, + state: stateTx, + } = this.buildFloraTopicCreateTxs({ floraAccountId, keyList, submitList, autoRenewAccountId: params.autoRenewAccountId, }); - const commR = await (await (await commTx.freezeWithSigner(signer)).executeWithSigner(signer)).getReceiptWithSigner( - signer, - ); - const trnR = await (await (await trnTx.freezeWithSigner(signer)).executeWithSigner(signer)).getReceiptWithSigner( - signer, - ); - const stateR = await (await (await stateTx.freezeWithSigner(signer)).executeWithSigner(signer)).getReceiptWithSigner( - signer, - ); + const commR = await ( + await (await commTx.freezeWithSigner(signer)).executeWithSigner(signer) + ).getReceiptWithSigner(signer); + const trnR = await ( + await (await trnTx.freezeWithSigner(signer)).executeWithSigner(signer) + ).getReceiptWithSigner(signer); + const stateR = await ( + await (await stateTx.freezeWithSigner(signer)).executeWithSigner(signer) + ).getReceiptWithSigner(signer); const topics = { communication: commR?.topicId?.toString?.() || '', transaction: trnR?.topicId?.toString?.() || '', @@ -195,8 +283,6 @@ export class HCS16BrowserClient extends HCS16BaseClient { await frozen.executeWithSigner(signer); } - - /** * Post flora_join_request on Flora communication topic. * If submitKey=1/M, a member must relay the message. @@ -204,7 +290,11 @@ export class HCS16BrowserClient extends HCS16BaseClient { async sendFloraJoinRequest(params: { topicId: string; operatorId: string; - candidateAccountId: string; + accountId: string; + connectionRequestId: number; + connectionTopicId: string; + connectionSeq: number; + memo?: string; }): Promise { const signer = this.getSigner(); const tx = buildHcs16FloraJoinRequestTx(params); @@ -216,8 +306,11 @@ export class HCS16BrowserClient extends HCS16BaseClient { async sendFloraJoinVote(params: { topicId: string; operatorId: string; - candidateAccountId: string; + accountId: string; approve: boolean; + connectionRequestId: number; + connectionSeq: number; + memo?: string; }): Promise { const signer = this.getSigner(); const tx = buildHcs16FloraJoinVoteTx(params); @@ -230,7 +323,8 @@ export class HCS16BrowserClient extends HCS16BaseClient { topicId: string; operatorId: string; members: string[]; - epoch?: number; + epoch: number; + memo?: string; }): Promise { const signer = this.getSigner(); const tx = buildHcs16FloraJoinAcceptedTx(params); diff --git a/src/hcs-16/schemas.ts b/src/hcs-16/schemas.ts new file mode 100644 index 00000000..5555a2b9 --- /dev/null +++ b/src/hcs-16/schemas.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import { FloraOperationCode, FloraTopicType } from './types'; + +export const OpMemoParamsSchema = z.object({ + opCode: z.nativeEnum(FloraOperationCode).optional(), + topicTypeHint: z.nativeEnum(FloraTopicType).optional(), +}); + +export const FloraMemberSchema = z.object({ + accountId: z.string(), + publicKey: z.string().optional(), + weight: z.number().optional(), +}); + +export const FloraProfileSchema = z.object({ + version: z.string(), + type: z.literal(3), + display_name: z.string().min(1), + members: z.array(FloraMemberSchema).min(1), + threshold: z.number().min(1), + topics: z.object({ + communication: z.string(), + transaction: z.string(), + state: z.string(), + }), + inboundTopicId: z.string(), + outboundTopicId: z.string(), + alias: z.string().optional(), + bio: z.string().optional(), + socials: z + .array( + z.object({ + platform: z.string(), + handle: z.string(), + }), + ) + .optional(), + profileImage: z.string().optional(), + properties: z.record(z.any()).optional(), + policies: z.record(z.any()).optional(), + metadata: z.record(z.any()).optional(), +}); + +export type FloraProfileInput = z.infer; diff --git a/src/hcs-16/sdk.ts b/src/hcs-16/sdk.ts index 534985c4..bcadf76b 100644 --- a/src/hcs-16/sdk.ts +++ b/src/hcs-16/sdk.ts @@ -7,6 +7,7 @@ import { Hbar, ScheduleSignTransaction, PrivateKey, + Transaction, } from '@hashgraph/sdk'; import type { NetworkType } from '../utils/types'; import type { Logger } from '../utils/logger'; @@ -16,9 +17,29 @@ import { } from '../common/node-operator-resolver'; import { HederaMirrorNode } from '../services/mirror-node'; import { HCS16BaseClient } from './base-client'; -import { buildHcs16CreateFloraTopicTx, buildHcs16FloraCreatedTx, buildHcs16TransactionTx, buildHcs16StateUpdateTx } from './tx'; +import { + buildHcs16CreateFloraTopicTx, + buildHcs16FloraCreatedTx, + buildHcs16TransactionTx, + buildHcs16StateUpdateTx, + buildHcs16StateHashTx, +} from './tx'; import { FloraTopicType } from './types'; -import { buildHcs16FloraJoinRequestTx, buildHcs16FloraJoinVoteTx, buildHcs16FloraJoinAcceptedTx, buildHcs16CreateAccountTx } from './tx'; +import { + buildHcs16FloraJoinRequestTx, + buildHcs16FloraJoinVoteTx, + buildHcs16FloraJoinAcceptedTx, + buildHcs16CreateAccountTx, +} from './tx'; +import { + buildHcs16ScheduleAccountKeyUpdateTx, + buildHcs16ScheduleTopicKeyUpdateTx, + buildHcs16ScheduleAccountDeleteTx, +} from './tx'; +import type { FloraProfile } from './types'; +import { HCS11Client } from '../hcs-11/client'; +import { FloraProfileSchema } from './schemas'; +import { buildHcs16UpdateFloraMemoToProfileTx } from './tx'; export interface HCS16ClientConfig { network: NetworkType; @@ -55,6 +76,18 @@ export class HCS16Client extends HCS16BaseClient { this.client = this.operatorCtx.client; } + private async executeWithOptionalSigner( + tx: T, + signerKey?: PrivateKey, + ): Promise { + const frozen = tx.freezeWith(this.client); + if (signerKey) { + await frozen.sign(signerKey); + } + const resp = await frozen.execute(this.client); + return resp.getReceipt(this.client); + } + /** * Create a Flora topic with memo `hcs-16::`. */ @@ -64,16 +97,34 @@ export class HCS16Client extends HCS16BaseClient { adminKey?: PublicKey | KeyList; submitKey?: PublicKey | KeyList; autoRenewAccountId?: string; + signerKeys?: PrivateKey[]; }): Promise { + const { + floraAccountId, + topicType, + adminKey, + submitKey, + autoRenewAccountId, + signerKeys, + } = params; + const tx = buildHcs16CreateFloraTopicTx({ - floraAccountId: params.floraAccountId, - topicType: params.topicType, - adminKey: params.adminKey, - submitKey: params.submitKey, + floraAccountId, + topicType, + adminKey, + submitKey, operatorPublicKey: this.client.operatorPublicKey || undefined, - autoRenewAccountId: params.autoRenewAccountId, + autoRenewAccountId, }); - const resp = await tx.execute(this.client); + + const frozen = await tx.freezeWith(this.client); + if (signerKeys?.length) { + for (const key of signerKeys) { + await frozen.sign(key); + } + } + + const resp = await frozen.execute(this.client); const receipt = await resp.getReceipt(this.client); if (!receipt.topicId) { throw new Error('Failed to create Flora topic'); @@ -86,10 +137,11 @@ export class HCS16Client extends HCS16BaseClient { operatorId: string; floraAccountId: string; topics: { communication: string; transaction: string; state: string }; + signerKey?: PrivateKey; }): Promise { - const tx = buildHcs16FloraCreatedTx(params); - const resp = await tx.execute(this.client); - return resp.getReceipt(this.client); + const { signerKey, ...rest } = params; + const tx = buildHcs16FloraCreatedTx(rest); + return this.executeWithOptionalSigner(tx, signerKey); } /** @@ -100,18 +152,24 @@ export class HCS16Client extends HCS16BaseClient { operatorId: string; scheduleId: string; data?: string; + signerKey?: PrivateKey; }): Promise { - const tx = buildHcs16TransactionTx(params); - const resp = await tx.execute(this.client); - return resp.getReceipt(this.client); + const { signerKey, ...rest } = params; + const tx = buildHcs16TransactionTx(rest); + return this.executeWithOptionalSigner(tx, signerKey); } /** * Sign a scheduled transaction by ScheduleId entity using provided signer key (PrivateKey). * The signer must be a valid member key for the scheduled transaction to count toward threshold. */ - async signSchedule(params: { scheduleId: string; signerKey: PrivateKey }): Promise { - const tx = await new ScheduleSignTransaction().setScheduleId(params.scheduleId).freezeWith(this.client); + async signSchedule(params: { + scheduleId: string; + signerKey: PrivateKey; + }): Promise { + const tx = await new ScheduleSignTransaction() + .setScheduleId(params.scheduleId) + .freezeWith(this.client); const signed = await tx.sign(params.signerKey); const resp = await signed.execute(this.client); return resp.getReceipt(this.client); @@ -122,10 +180,24 @@ export class HCS16Client extends HCS16BaseClient { operatorId: string; hash: string; epoch?: number; + signerKey?: PrivateKey; }): Promise { - const tx = buildHcs16StateUpdateTx(params); - const resp = await tx.execute(this.client); - return resp.getReceipt(this.client); + const { signerKey, ...rest } = params; + const tx = buildHcs16StateUpdateTx(rest); + return this.executeWithOptionalSigner(tx, signerKey); + } + + async sendStateHash(params: { + topicId: string; + stateHash: string; + accountId: string; + topics: string[]; + memo?: string; + signerKey?: PrivateKey; + }): Promise { + const { signerKey, ...rest } = params; + const tx = buildHcs16StateHashTx(rest); + return this.executeWithOptionalSigner(tx, signerKey); } /** @@ -219,5 +291,89 @@ export class HCS16Client extends HCS16BaseClient { return resp.getReceipt(this.client); } - + /** + * Update Flora account memo to point at an HCS-11 profile resource (hcs-11:). + */ + async updateFloraAccountMemoToProfile(params: { + floraAccountId: string; + profileResource: string; + }): Promise { + const tx = buildHcs16UpdateFloraMemoToProfileTx({ + floraAccountId: params.floraAccountId, + profileResource: params.profileResource, + }); + const resp = await tx.execute(this.client); + return resp.getReceipt(this.client); + } + + /** + * Publish Flora profile via provided HCS-11 client and update Flora memo to reference it. + * Caller constructs `profile` and HCS11Client (to avoid circular SDK wiring). + */ + async publishFloraProfileAndMemo(params: { + hcs11: HCS11Client; + floraAccountId: string; + profile: FloraProfile | any; + }): Promise<{ profileResource: string; receipt: TransactionReceipt }> { + const parsed = FloraProfileSchema.parse(params.profile); + const result = await params.hcs11.inscribeProfile(parsed as any); + if (!result.success) { + throw new Error( + `Failed to inscribe HCS-11 profile: ${result.error || 'unknown'}`, + ); + } + const resource = `hcs://1/${result.profileTopicId}`; + const receipt = await this.updateFloraAccountMemoToProfile({ + floraAccountId: params.floraAccountId, + profileResource: resource, + }); + return { profileResource: resource, receipt }; + } + + /** Create a schedule to update Flora account KeyList (membership change). */ + async scheduleAccountKeyUpdate(params: { + floraAccountId: string; + newKeyList: KeyList; + memo?: string; + }): Promise { + const tx = buildHcs16ScheduleAccountKeyUpdateTx({ + floraAccountId: params.floraAccountId, + newKeyList: params.newKeyList, + memo: params.memo, + }); + const resp = await tx.execute(this.client); + return resp.getReceipt(this.client); + } + + /** Create a schedule to update topic admin/submit keys. */ + async scheduleTopicKeyUpdate(params: { + topicId: string; + adminKey?: PublicKey | KeyList; + submitKey?: PublicKey | KeyList; + memo?: string; + }): Promise { + const tx = buildHcs16ScheduleTopicKeyUpdateTx({ + topicId: params.topicId, + adminKey: params.adminKey, + submitKey: params.submitKey, + memo: params.memo, + }); + const resp = await tx.execute(this.client); + return resp.getReceipt(this.client); + } + + /** Schedule Flora account deletion with transfer to beneficiary (requires cleanup preconditions). */ + async scheduleFloraDeletion(params: { + floraAccountId: string; + transferAccountId: string; + memo?: string; + }): Promise { + const tx = buildHcs16ScheduleAccountDeleteTx({ + floraAccountId: params.floraAccountId, + transferAccountId: params.transferAccountId, + memo: params.memo, + }); + const resp = await tx.execute(this.client); + return resp.getReceipt(this.client); + } } diff --git a/src/hcs-16/tx.ts b/src/hcs-16/tx.ts index 1c9fc78e..ebd635d7 100644 --- a/src/hcs-16/tx.ts +++ b/src/hcs-16/tx.ts @@ -12,12 +12,20 @@ import { CustomFixedFee, TokenId, } from '@hashgraph/sdk'; +import * as Hashgraph from '@hashgraph/sdk'; import { buildTopicCreateTx, buildMessageTx, type MaybeKey, } from '../common/tx/tx-utils'; -import { FloraOperation, FloraTopicType, type FloraMessage } from './types'; +import { buildHcs17MessageTx } from '../hcs-17/tx'; +import { + FloraOperation, + FloraTopicType, + FloraOperationCode, + type FloraMessage, +} from './types'; +import { OpMemoParamsSchema } from './schemas'; function encodeHcs16FloraMemo(params: { floraAccountId: string; @@ -26,6 +34,8 @@ function encodeHcs16FloraMemo(params: { return `hcs-16:${params.floraAccountId}:${params.topicType}`; } +const HCS17_STATE_HASH_MEMO = `hcs-17:op:${FloraOperationCode.STATE_HASH}:${FloraTopicType.STATE}`; + /** * Build a TopicCreateTransaction for HCS‑16 Flora topics (communication/transaction/state). */ @@ -170,6 +180,8 @@ export function buildHcs16MessageTx(params: { operatorId: string; op: FloraOperation; body?: Record; + opCode?: FloraOperationCode; + topicTypeHint?: FloraTopicType; }): TopicMessageSubmitTransaction { const payload: FloraMessage = { p: 'hcs-16', @@ -178,9 +190,26 @@ export function buildHcs16MessageTx(params: { ...(params.body || {}), } as FloraMessage; + let transactionMemo: string | undefined; + const parsed = OpMemoParamsSchema.safeParse({ + opCode: params.opCode, + topicTypeHint: params.topicTypeHint, + }); + if ( + parsed.success && + parsed.data.opCode !== undefined && + parsed.data.topicTypeHint !== undefined + ) { + transactionMemo = encodeHcs16OpMemo( + parsed.data.opCode, + parsed.data.topicTypeHint, + ); + } + return buildMessageTx({ topicId: params.topicId, message: JSON.stringify(payload), + transactionMemo, }); } @@ -197,6 +226,8 @@ export function buildHcs16FloraCreatedTx(params: { topicId: params.topicId, operatorId: params.operatorId, op: FloraOperation.FLORA_CREATED, + opCode: FloraOperationCode.FLORA_CREATED, + topicTypeHint: FloraTopicType.COMMUNICATION, body: { flora_account_id: params.floraAccountId, topics: params.topics, @@ -217,6 +248,8 @@ export function buildHcs16TransactionTx(params: { topicId: params.topicId, operatorId: params.operatorId, op: FloraOperation.TRANSACTION, + opCode: FloraOperationCode.TRANSACTION, + topicTypeHint: FloraTopicType.TRANSACTION, body: { schedule_id: params.scheduleId, data: params.data, @@ -225,8 +258,6 @@ export function buildHcs16TransactionTx(params: { }); } - - /** * Build HCS‑16 state_update message. */ @@ -240,6 +271,8 @@ export function buildHcs16StateUpdateTx(params: { topicId: params.topicId, operatorId: params.operatorId, op: FloraOperation.STATE_UPDATE, + opCode: FloraOperationCode.STATE_UPDATE, + topicTypeHint: FloraTopicType.STATE, body: { hash: params.hash, epoch: params.epoch, @@ -248,20 +281,50 @@ export function buildHcs16StateUpdateTx(params: { }); } +/** + * Build HCS-17 state_hash message for Flora state commitments. + */ +export function buildHcs16StateHashTx(params: { + topicId: string; + stateHash: string; + accountId: string; + topics: string[]; + memo?: string; +}): TopicMessageSubmitTransaction { + return buildHcs17MessageTx({ + topicId: params.topicId, + stateHash: params.stateHash, + accountId: params.accountId, + topics: params.topics, + memo: params.memo, + transactionMemo: HCS17_STATE_HASH_MEMO, + }); +} + /** * Build HCS‑16 flora_join_request message. */ export function buildHcs16FloraJoinRequestTx(params: { topicId: string; operatorId: string; - candidateAccountId: string; + accountId: string; + connectionRequestId: number; + connectionTopicId: string; + connectionSeq: number; + memo?: string; }): TopicMessageSubmitTransaction { return buildHcs16MessageTx({ topicId: params.topicId, operatorId: params.operatorId, op: FloraOperation.FLORA_JOIN_REQUEST, + opCode: FloraOperationCode.FLORA_JOIN_REQUEST, + topicTypeHint: FloraTopicType.COMMUNICATION, body: { - candidate_account_id: params.candidateAccountId, + account_id: params.accountId, + connection_request_id: params.connectionRequestId, + connection_topic_id: params.connectionTopicId, + connection_seq: params.connectionSeq, + ...(params.memo ? { m: params.memo } : {}), }, }); } @@ -272,16 +335,24 @@ export function buildHcs16FloraJoinRequestTx(params: { export function buildHcs16FloraJoinVoteTx(params: { topicId: string; operatorId: string; - candidateAccountId: string; + accountId: string; approve: boolean; + connectionRequestId: number; + connectionSeq: number; + memo?: string; }): TopicMessageSubmitTransaction { return buildHcs16MessageTx({ topicId: params.topicId, operatorId: params.operatorId, op: FloraOperation.FLORA_JOIN_VOTE, + opCode: FloraOperationCode.FLORA_JOIN_VOTE, + topicTypeHint: FloraTopicType.COMMUNICATION, body: { - candidate_account_id: params.candidateAccountId, + account_id: params.accountId, approve: params.approve, + connection_request_id: params.connectionRequestId, + connection_seq: params.connectionSeq, + ...(params.memo ? { m: params.memo } : {}), }, }); } @@ -293,15 +364,59 @@ export function buildHcs16FloraJoinAcceptedTx(params: { topicId: string; operatorId: string; members: string[]; - epoch?: number; + epoch: number; + memo?: string; }): TopicMessageSubmitTransaction { return buildHcs16MessageTx({ topicId: params.topicId, operatorId: params.operatorId, op: FloraOperation.FLORA_JOIN_ACCEPTED, + opCode: FloraOperationCode.FLORA_JOIN_ACCEPTED, + topicTypeHint: FloraTopicType.STATE, body: { members: params.members, epoch: params.epoch, + ...(params.memo ? { m: params.memo } : {}), }, }); } + +/** Encode recommended op memo hcs-16:op:: */ +function encodeHcs16OpMemo( + opCode: FloraOperationCode, + topicType: FloraTopicType, +): string { + return `hcs-16:op:${opCode}:${topicType}`; +} + +/** Schedule delete of a Flora account (AccountDeleteTransaction). */ +export function buildHcs16ScheduleAccountDeleteTx(params: { + floraAccountId: string; + transferAccountId: string; + memo?: string; +}): ScheduleCreateTransaction { + // Use real AccountDeleteTransaction; access via namespace import to avoid typing gaps in some SDK versions + const DeleteCtor = (Hashgraph as any).AccountDeleteTransaction; + if (!DeleteCtor) { + throw new Error( + 'AccountDeleteTransaction is not available in @hashgraph/sdk', + ); + } + const inner = new DeleteCtor(); + inner.setAccountId(AccountId.fromString(params.floraAccountId)); + inner.setTransferAccountId(AccountId.fromString(params.transferAccountId)); + if (params.memo) { + inner.setTransactionMemo(params.memo); + } + return new ScheduleCreateTransaction().setScheduledTransaction(inner); +} + +/** Build an AccountUpdateTransaction for updating Flora memo to reference an HCS-11 profile */ +export function buildHcs16UpdateFloraMemoToProfileTx(params: { + floraAccountId: string; + profileResource: string; +}): AccountUpdateTransaction { + return new AccountUpdateTransaction() + .setAccountId(params.floraAccountId) + .setAccountMemo(`hcs-11:${params.profileResource}`); +} diff --git a/src/hcs-16/types.ts b/src/hcs-16/types.ts index 34668136..09845d0b 100644 --- a/src/hcs-16/types.ts +++ b/src/hcs-16/types.ts @@ -85,6 +85,27 @@ export enum FloraOperation { FLORA_JOIN_ACCEPTED = 'flora_join_accepted', } +/** + * Numeric operation codes for recommended memo encoding (hcs-16:op::). + * These align with the specification's operation table. + * 0: flora_created (CTopic) + * 1: transaction (TTopic) + * 2: state_update (STopic) + * 3: flora_join_request (CTopic) + * 4: flora_join_vote (CTopic) + * 5: flora_join_accepted (STopic) + * 6: state_hash (STopic, emitted via HCS-17 envelope) + */ +export enum FloraOperationCode { + FLORA_CREATED = 0, + TRANSACTION = 1, + STATE_UPDATE = 2, + FLORA_JOIN_REQUEST = 3, + FLORA_JOIN_VOTE = 4, + FLORA_JOIN_ACCEPTED = 5, + STATE_HASH = 6, +} + /** * HCS-16 Message envelope */