import * as dotenv from "dotenv";
import * as path from "path";
import { IotaClient, getFullnodeUrl } from "@iota/iota-sdk/client";
import { Transaction } from "@iota/iota-sdk/transactions";
import { Ed25519PublicKey } from "@iota/iota-sdk/keypairs/ed25519";
import { messageWithIntent } from "@iota/iota-sdk/cryptography";
import { Turnkey } from "@turnkey/sdk-server";
import { blake2b } from "@noble/hashes/blake2b";
import { bytesToHex } from "@noble/hashes/utils";
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
function toSerializedSignature({
signature,
pubKey,
}: {
signature: Uint8Array;
pubKey: Ed25519PublicKey;
}): string {
const scheme = new Uint8Array([0x00]); // ED25519 flag
const pubKeyBytes = pubKey.toRawBytes();
const serialized = new Uint8Array(
scheme.length + signature.length + pubKeyBytes.length,
);
serialized.set(scheme, 0);
serialized.set(signature, scheme.length);
serialized.set(pubKeyBytes, scheme.length + signature.length);
return Buffer.from(serialized).toString("base64");
}
async function main() {
// load the variables from .env
// IOTA_ADDRESS and IOTA_PUBLIC_KEY of the Turnkey signer
const {
ORGANIZATION_ID,
API_PRIVATE_KEY,
API_PUBLIC_KEY,
IOTA_ADDRESS,
IOTA_PUBLIC_KEY,
} = process.env;
if (IOTA_ADDRESS === undefined || IOTA_PUBLIC_KEY === undefined) {
throw new Error("IOTA_ADDRESS or IOTA_PUBLIC_KEY not set in .env.local");
}
// sending to the same address
const recipient = IOTA_ADDRESS;
const amount = 1_000_000n; // 0.001 IOTA
const turnkeyClient = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiPrivateKey: API_PRIVATE_KEY!,
apiPublicKey: API_PUBLIC_KEY!,
defaultOrganizationId: ORGANIZATION_ID!,
});
const provider = new IotaClient({ url: getFullnodeUrl("testnet") });
const publicKey = new Ed25519PublicKey(Buffer.from(IOTA_PUBLIC_KEY!, "hex"));
// if (publicKey.toIotaAddress() !== IOTA_ADDRESS) {
// throw new Error('IOTA_PUBLIC_KEY does not match IOTA_ADDRESS');
// }
// fetch the user's IOTA coin objects
const coins = await provider.getCoins({
owner: IOTA_ADDRESS!,
coinType: "0x2::iota::IOTA",
});
if (!coins.data.length) throw new Error("No IOTA coins");
const tx = new Transaction();
tx.setSender(IOTA_ADDRESS!);
tx.setGasPrice(await provider.getReferenceGasPrice());
tx.setGasBudget(5_000_000n);
tx.setGasPayment([
{
objectId: coins.data[0]!.coinObjectId,
version: coins.data[0]!.version,
digest: coins.data[0]!.digest,
},
]);
const coin = tx.splitCoins(tx.gas, [tx.pure("u64", amount)]);
tx.transferObjects([coin], tx.pure.address(recipient));
const txBytes = await tx.build();
const intentMsg = messageWithIntent("TransactionData", txBytes);
const digest = blake2b(intentMsg, { dkLen: 32 });
const { r, s } = await turnkeyClient.apiClient().signRawPayload({
signWith: IOTA_ADDRESS!,
payload: bytesToHex(digest),
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE",
});
const signature = Buffer.from(r + s, "hex");
const serialized = toSerializedSignature({ signature, pubKey: publicKey });
const result = await provider.executeTransactionBlock({
transactionBlock: Buffer.from(txBytes).toString("base64"),
signature: serialized,
requestType: "WaitForEffectsCert",
options: { showEffects: true },
});
console.log("Transaction digest:", result.digest);
}
main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});