@hashgraphonline/standards-sdk
Version:
The Hashgraph Online Standards SDK provides a complete implementation of the Hashgraph Consensus Standards (HCS), giving developers all the tools needed to build applications on Hedera.
506 lines (505 loc) • 15.8 kB
JavaScript
import { AccountId, Client, PrivateKey, TopicMessageSubmitTransaction, TopicId, Status, TopicCreateTransaction } from "@hashgraph/sdk";
import { HCS20BaseClient } from "./standards-sdk.es18.js";
import { PointsDeploymentError, PointsValidationError, PointsTransferError, PointsBurnError } from "./standards-sdk.es17.js";
import { sleep } from "./standards-sdk.es32.js";
import { detectKeyTypeFromString } from "./standards-sdk.es27.js";
class HCS20Client extends HCS20BaseClient {
constructor(config) {
super(config);
this.initialized = false;
this.operatorId = typeof config.operatorId === "string" ? AccountId.fromString(config.operatorId) : config.operatorId;
this.operatorKeyString = config.operatorKey;
this.client = this.network === "mainnet" ? Client.forMainnet() : Client.forTestnet();
try {
const { privateKey, detectedType } = detectKeyTypeFromString(
config.operatorKey
);
this.operatorKey = privateKey;
this.keyType = detectedType;
this.client.setOperator(this.operatorId, this.operatorKey);
this.initialized = true;
} catch (error) {
this.logger.debug(
"Failed to detect key type from string, will initialize later"
);
}
}
/**
* Initialize operator by querying mirror node for key type
*/
async initializeOperator() {
if (this.initialized) return;
try {
const accountInfo = await this.mirrorNode.requestAccount(
this.operatorId.toString()
);
const keyType = accountInfo?.key?._type;
if (keyType?.includes("ECDSA")) {
this.keyType = "ecdsa";
} else if (keyType?.includes("ED25519")) {
this.keyType = "ed25519";
} else {
this.keyType = "ed25519";
}
this.operatorKey = this.keyType === "ecdsa" ? PrivateKey.fromStringECDSA(this.operatorKeyString) : PrivateKey.fromStringED25519(this.operatorKeyString);
this.client.setOperator(this.operatorId, this.operatorKey);
this.initialized = true;
this.logger.debug(`Initialized operator with key type: ${this.keyType}`);
} catch (error) {
this.logger.warn(
"Failed to query mirror node for key type, using ED25519"
);
this.keyType = "ed25519";
this.operatorKey = PrivateKey.fromStringED25519(this.operatorKeyString);
this.client.setOperator(this.operatorId, this.operatorKey);
this.initialized = true;
}
}
/**
* Ensure operator is initialized before operations
*/
async ensureInitialized() {
if (!this.initialized) {
await this.initializeOperator();
}
}
/**
* Submit a payload to a topic
*/
async submitPayload(topicId, payload, submitKey) {
const message = typeof payload === "string" ? payload : JSON.stringify(payload);
const transaction = new TopicMessageSubmitTransaction().setTopicId(TopicId.fromString(topicId)).setMessage(message);
let transactionResponse;
if (submitKey) {
const frozenTransaction = transaction.freezeWith(this.client);
const signedTransaction = await frozenTransaction.sign(submitKey);
transactionResponse = await signedTransaction.execute(this.client);
} else {
transactionResponse = await transaction.execute(this.client);
}
const receipt = await transactionResponse.getReceipt(this.client);
if (!receipt || receipt.status !== Status.Success) {
throw new Error("Failed to submit message to topic");
}
return {
receipt,
transactionId: transactionResponse.transactionId.toString()
};
}
/**
* Create a public topic for HCS-20 (for testnet)
*/
async createPublicTopic(memo) {
await this.ensureInitialized();
this.logger.info("Creating public HCS-20 topic...");
const topicCreateTx = await new TopicCreateTransaction().setTopicMemo(memo || "HCS-20 Public Topic").execute(this.client);
const receipt = await topicCreateTx.getReceipt(this.client);
if (receipt.status !== Status.Success || !receipt.topicId) {
throw new Error("Failed to create public topic");
}
const topicId = receipt.topicId.toString();
this.logger.info(`Created public topic: ${topicId}`);
this.publicTopicId = topicId;
return topicId;
}
/**
* Create a registry topic for HCS-20
*/
async createRegistryTopic(memo) {
await this.ensureInitialized();
this.logger.info("Creating HCS-20 registry topic...");
const topicCreateTx = await new TopicCreateTransaction().setTopicMemo(memo || "HCS-20 Registry").execute(this.client);
const receipt = await topicCreateTx.getReceipt(this.client);
if (receipt.status !== Status.Success || !receipt.topicId) {
throw new Error("Failed to create registry topic");
}
const topicId = receipt.topicId.toString();
this.logger.info(`Created registry topic: ${topicId}`);
this.registryTopicId = topicId;
return topicId;
}
/**
* Deploy new points
*/
async deployPoints(options) {
await this.ensureInitialized();
const { progressCallback } = options;
try {
progressCallback?.({
stage: "creating-topic",
percentage: 20
});
let topicId;
if (options.usePrivateTopic) {
const topicCreateTx = await new TopicCreateTransaction().setTopicMemo(options.topicMemo || `HCS-20: ${options.name}`).setSubmitKey(this.operatorKey.publicKey).setAdminKey(this.operatorKey.publicKey).execute(this.client);
const receipt = await topicCreateTx.getReceipt(this.client);
if (receipt.status !== Status.Success || !receipt.topicId) {
throw new PointsDeploymentError(
"Failed to create topic",
options.tick
);
}
topicId = receipt.topicId.toString();
this.logger.info(`Created private topic: ${topicId}`);
} else {
topicId = this.publicTopicId;
}
progressCallback?.({
stage: "submitting-deploy",
percentage: 50,
topicId
});
const deployMessage = {
p: "hcs-20",
op: "deploy",
name: options.name,
tick: this.normalizeTick(options.tick),
max: options.maxSupply,
lim: options.limitPerMint,
metadata: options.metadata,
m: options.topicMemo
};
const validation = this.validateMessage(deployMessage);
if (!validation.valid) {
throw new PointsValidationError(
"Invalid deploy message",
validation.errors
);
}
const { transactionId: deployTxId } = await this.submitPayload(
topicId,
deployMessage
);
progressCallback?.({
stage: "confirming",
percentage: 80,
topicId,
deployTxId
});
await this.waitForMirrorNodeConfirmation(topicId, deployTxId);
progressCallback?.({
stage: "complete",
percentage: 100,
topicId,
deployTxId
});
const pointsInfo = {
name: options.name,
tick: this.normalizeTick(options.tick),
maxSupply: options.maxSupply,
limitPerMint: options.limitPerMint,
metadata: options.metadata,
topicId,
deployerAccountId: this.operatorId.toString(),
currentSupply: "0",
deploymentTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
isPrivate: options.usePrivateTopic || false
};
return pointsInfo;
} catch (error) {
progressCallback?.({
stage: "complete",
percentage: 100,
error: error instanceof Error ? error.message : "Unknown error"
});
throw error;
}
}
/**
* Mint points
*/
async mintPoints(options) {
await this.ensureInitialized();
const { progressCallback } = options;
try {
progressCallback?.({
stage: "validating",
percentage: 20
});
const normalizedTick = this.normalizeTick(options.tick);
progressCallback?.({
stage: "submitting",
percentage: 50
});
const mintMessage = {
p: "hcs-20",
op: "mint",
tick: normalizedTick,
amt: options.amount,
to: this.accountToString(options.to),
m: options.memo
};
const topicId = options.topicId || this.publicTopicId;
const { transactionId: mintTxId } = await this.submitPayload(
topicId,
mintMessage
);
progressCallback?.({
stage: "confirming",
percentage: 80,
mintTxId
});
await this.waitForMirrorNodeConfirmation(topicId, mintTxId);
progressCallback?.({
stage: "complete",
percentage: 100,
mintTxId
});
const transaction = {
id: mintTxId,
operation: "mint",
tick: normalizedTick,
amount: options.amount,
to: this.accountToString(options.to),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
sequenceNumber: 0,
topicId,
transactionId: mintTxId,
memo: options.memo
};
return transaction;
} catch (error) {
progressCallback?.({
stage: "complete",
percentage: 100,
error: error instanceof Error ? error.message : "Unknown error"
});
throw error;
}
}
/**
* Transfer points
*/
async transferPoints(options) {
await this.ensureInitialized();
const { progressCallback } = options;
try {
progressCallback?.({
stage: "validating-balance",
percentage: 20
});
const normalizedTick = this.normalizeTick(options.tick);
const fromAccount = this.accountToString(options.from);
const toAccount = this.accountToString(options.to);
if (fromAccount !== this.operatorId.toString()) {
throw new PointsTransferError(
"For public topics, transaction payer must match sender",
options.tick,
fromAccount,
toAccount,
options.amount
);
}
progressCallback?.({
stage: "submitting",
percentage: 50
});
const transferMessage = {
p: "hcs-20",
op: "transfer",
tick: normalizedTick,
amt: options.amount,
from: fromAccount,
to: toAccount,
m: options.memo
};
const topicId = options.topicId || this.publicTopicId;
const { transactionId: transferTxId } = await this.submitPayload(
topicId,
transferMessage
);
progressCallback?.({
stage: "confirming",
percentage: 80,
transferTxId
});
await this.waitForMirrorNodeConfirmation(topicId, transferTxId);
progressCallback?.({
stage: "complete",
percentage: 100,
transferTxId
});
const transaction = {
id: transferTxId,
operation: "transfer",
tick: normalizedTick,
amount: options.amount,
from: fromAccount,
to: toAccount,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
sequenceNumber: 0,
topicId,
transactionId: transferTxId,
memo: options.memo
};
return transaction;
} catch (error) {
progressCallback?.({
stage: "complete",
percentage: 100,
error: error instanceof Error ? error.message : "Unknown error"
});
throw error;
}
}
/**
* Burn points
*/
async burnPoints(options) {
await this.ensureInitialized();
const { progressCallback } = options;
try {
progressCallback?.({
stage: "validating-balance",
percentage: 20
});
const normalizedTick = this.normalizeTick(options.tick);
const fromAccount = this.accountToString(options.from);
if (fromAccount !== this.operatorId.toString()) {
throw new PointsBurnError(
"For public topics, transaction payer must match burner",
options.tick,
fromAccount,
options.amount
);
}
progressCallback?.({
stage: "submitting",
percentage: 50
});
const burnMessage = {
p: "hcs-20",
op: "burn",
tick: normalizedTick,
amt: options.amount,
from: fromAccount,
m: options.memo
};
const topicId = options.topicId || this.publicTopicId;
const { transactionId: burnTxId } = await this.submitPayload(
topicId,
burnMessage
);
progressCallback?.({
stage: "confirming",
percentage: 80,
burnTxId
});
await this.waitForMirrorNodeConfirmation(topicId, burnTxId);
progressCallback?.({
stage: "complete",
percentage: 100,
burnTxId
});
const transaction = {
id: burnTxId,
operation: "burn",
tick: normalizedTick,
amount: options.amount,
from: fromAccount,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
sequenceNumber: 0,
topicId,
transactionId: burnTxId,
memo: options.memo
};
return transaction;
} catch (error) {
progressCallback?.({
stage: "complete",
percentage: 100,
error: error instanceof Error ? error.message : "Unknown error"
});
throw error;
}
}
/**
* Register a topic in the registry
*/
async registerTopic(options) {
await this.ensureInitialized();
const { progressCallback } = options;
try {
progressCallback?.({
stage: "validating",
percentage: 20
});
const registerMessage = {
p: "hcs-20",
op: "register",
name: options.name,
metadata: options.metadata,
private: options.isPrivate,
t_id: this.topicToString(options.topicId),
m: options.memo
};
const validation = this.validateMessage(registerMessage);
if (!validation.valid) {
throw new PointsValidationError(
"Invalid register message",
validation.errors
);
}
progressCallback?.({
stage: "submitting",
percentage: 50
});
const { transactionId: registerTxId } = await this.submitPayload(
this.registryTopicId,
registerMessage
);
progressCallback?.({
stage: "confirming",
percentage: 80,
registerTxId
});
await this.waitForMirrorNodeConfirmation(
this.registryTopicId,
registerTxId
);
progressCallback?.({
stage: "complete",
percentage: 100,
registerTxId
});
this.logger.info(`Registered topic ${options.topicId} in registry`);
} catch (error) {
progressCallback?.({
stage: "complete",
percentage: 100,
error: error instanceof Error ? error.message : "Unknown error"
});
throw error;
}
}
/**
* Wait for mirror node to index a message
*/
async waitForMirrorNodeConfirmation(topicId, transactionId, maxRetries = 10) {
for (let i = 0; i < maxRetries; i++) {
try {
const messages = await this.mirrorNode.getTopicMessages(topicId, {
limit: 10,
order: "desc"
});
const found = messages.some((msg) => msg.consensus_timestamp);
if (found) {
this.logger.debug(
`Transaction ${transactionId} confirmed on mirror node`
);
return;
}
} catch (error) {
this.logger.debug(`Mirror node check attempt ${i + 1} failed:`, error);
}
await sleep(2e3);
}
this.logger.warn(
`Transaction ${transactionId} not found on mirror node after ${maxRetries} attempts`
);
}
}
export {
HCS20Client
};
//# sourceMappingURL=standards-sdk.es20.js.map