import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
import {
broadcastTransaction,
createMessageSignature,
makeUnsignedSTXTokenTransfer,
sigHashPreSign,
SingleSigSpendingCondition,
TransactionSigner,
publicKeyToAddress,
type StacksTransactionWire,
} from "@stacks/transactions";
import * as path from "path";
import * as dotenv from "dotenv";
// Load environment variables from `.env.local`
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
// Define the Turnkey API client
const client = new TurnkeyServerSDK({
apiBaseUrl: process.env.TURNKEY_BASE_URL!,
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
});
// Construct an unsigned Stacks transaction
const constructStacksTx = async (pubKey: string) => {
const recipient = process.env.STACKS_RECIPIENT_ADDRESS!;
let transaction = await makeUnsignedSTXTokenTransfer({
recipient,
amount: 300,
publicKey: pubKey,
numSignatures: 1,
network: "mainnet",
});
return { stacksTransaction: transaction, sighash: transaction.signBegin() };
};
// The resulting preSignSigHash is what ultimately gets signed by a private key using ECDSA over the secp256k1 curve.
const generatePreSignSigHash = (
transaction: StacksTransactionWire,
sighash: string
) => {
let preSignSigHash = sigHashPreSign(
sigHash,
transaction.auth.authType,
transaction.auth.spendingCondition.fee,
transaction.auth.spendingCondition.nonce,
);
return preSignSigHash;
};
const signStacksTx = async () => {
try {
// Grab the public key associated with the user's Turnkey Ethereum account as a proxy
// Ethereum's public key generation and signing curve matches that of Stacks
// Use `publicKeyToAddress` to convert public key to Stacks address for display purposes
const stacksPublicKey = process.env.TURNKEY_SIGNER_PUBLIC_KEY!;
let { stacksTransaction, stacksTxSigner } = await constructStacksTx(
stacksPublicKey!,
);
let preSignSigHash = generatePreSignSigHash(
stacksTransaction,
stacksTxSigner,
);
const payload = `0x${preSignSigHash}`;
const signature = await client?.apiClient().signRawPayload({
payload,
// either passing in pubkey or account address
signWith: stacksPublicKey,
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NO_OP",
});
// r and s values returned are in hex format, may need to padStart r and s values
// v should be "00" for Stacks but the returned "01" also works
const nextSig = `${signature!.v}${signature!.r.padStart(64, "0")}${signature!.s.padStart(64, "0")}`;
// Reassign signature field in transaction with `nextSig`
let spendingCondition = stacksTransaction.auth.spendingCondition as SingleSigSpendingCondition;
spendingCondition.signature = createMessageSignature(nextSig);
return stacksTransaction;
} catch (err) {
console.error("Signing failed:", err);
return undefined;
}
};
const handleBroadcastTx = async () => {
let tx = await signStacksTx();
let result = await broadcastTransaction({
transaction: tx!,
network: "mainnet",
});
console.log("Broadcast Result:", result);
};
(async () => {
await handleBroadcastTx();
})();