import { Turnkey } from "@turnkey/sdk-server";
import { Curve } from "@turnkey/core";
import { uint8ArrayFromHexString } from "@turnkey/encoding";
import { createLedgerApiClient } from "@/api/ledger/client";
import { v7 as uuidv7 } from "uuid";
// Initialize the Turnkey client
const turnkey = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
});
const client = turnkey.apiClient();
// A Canton Ledger API client (local sandbox, or a live node via CANTON_LEDGER_API_URL)
const ledgerClient = createLedgerApiClient({
baseUrl: process.env.CANTON_LEDGER_API_URL || "http://localhost:6864",
});
// 1/ Create a wallet with an Ed25519 account.
// Canton keys use ADDRESS_FORMAT_COMPRESSED on the Ed25519 curve.
const { walletId } = await client.createWallet({
walletName: "Canton Wallet",
accounts: [
{
curve: Curve.ED25519,
pathFormat: "PATH_FORMAT_BIP32",
path: "m/44'/0'/0'/0/0",
addressFormat: "ADDRESS_FORMAT_COMPRESSED",
},
],
});
const { accounts } = await client.getWalletAccounts({ walletId });
const ed25519Account = accounts.find(({ curve }) => curve === Curve.ED25519)!;
// 2/ Register the public key with a Canton node to create a Party.
const { data: synchronizersData } = await ledgerClient.GET(
"/v2/state/connected-synchronizers",
);
const synchronizerId =
synchronizersData!.connectedSynchronizers![0].synchronizerId;
const keyData = Buffer.from(ed25519Account.publicKey!, "hex").toString("base64");
const { data: partyTopology } = await ledgerClient.POST(
"/v2/parties/external/generate-topology",
{
body: {
synchronizer: synchronizerId,
partyHint: `party-${uuidv7()}`,
publicKey: {
keySpec: "SIGNING_KEY_SPEC_EC_CURVE25519",
format: "CRYPTO_KEY_FORMAT_RAW",
keyData,
},
},
},
);
// Sign the topology multi-hash with Turnkey. Ed25519 signatures are the
// concatenation of r and s (there is no v component).
const multiHashPayload = Buffer.from(
partyTopology!.multiHash,
"base64",
).toString("hex");
const signedMultiHash = await client.signRawPayload({
signWith: ed25519Account.address,
payload: multiHashPayload,
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE",
});
const multiHashSignature = uint8ArrayFromHexString(
signedMultiHash.r + signedMultiHash.s,
);
// Allocate the party on the node. The topology result includes your PartyId
// and the Canton-internal fingerprint of your public key.
await ledgerClient.POST("/v2/parties/external/allocate", {
body: {
waitForAllocation: true,
synchronizer: synchronizerId,
onboardingTransactions: partyTopology!.topologyTransactions.map(
(transaction) => ({ transaction }),
),
multiHashSignatures: [
{
format: "SIGNATURE_FORMAT_CONCAT",
signature: Buffer.from(multiHashSignature).toString("base64"),
signedBy: partyTopology!.publicKeyFingerprint,
signingAlgorithmSpec: "SIGNING_ALGORITHM_SPEC_ED25519",
},
],
},
});
const partyId = partyTopology!.partyId;
// 3/ Create a user associated with the party.
const userId = `user-${uuidv7()}`;
await ledgerClient.POST("/v2/users", {
body: { user: { id: userId, partyId } },
});
// 4/ Prepare a transaction, then sign its hash with Turnkey.
const { data: prepared } = await ledgerClient.POST(
"/v2/interactive-submission/prepare",
{
body: {
commandId: `command-${uuidv7()}`,
synchronizerId,
userId,
actAs: [partyId],
hashingSchemeVersion: "HASHING_SCHEME_VERSION_V3",
commands: [
// ...your Daml commands, e.g. a CreateCommand on a template
],
},
},
);
// The Canton API returns a prepared transaction hash (base64). Convert it to
// hex and sign it with Turnkey (no additional hashing needed here).
const preparedTransactionHash = prepared!.preparedTransactionHash; // base64
const txPayload = Buffer.from(preparedTransactionHash, "base64").toString("hex");
const signedTx = await client.signRawPayload({
signWith: ed25519Account.address,
payload: txPayload,
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE",
});
const txSignature = uint8ArrayFromHexString(signedTx.r + signedTx.s);
// 5/ Execute the signed transaction.
await ledgerClient.POST(
"/v2/interactive-submission/executeAndWaitForTransaction",
{
body: {
preparedTransaction: prepared!.preparedTransaction,
submissionId: `submission-${uuidv7()}`,
userId,
hashingSchemeVersion: "HASHING_SCHEME_VERSION_V3",
deduplicationPeriod: { Empty: {} },
partySignatures: {
signatures: [
{
party: partyId,
signatures: [
{
format: "SIGNATURE_FORMAT_CONCAT",
signature: Buffer.from(txSignature).toString("base64"),
signedBy: partyTopology!.publicKeyFingerprint,
signingAlgorithmSpec: "SIGNING_ALGORITHM_SPEC_ED25519",
},
],
},
],
},
},
},
);