Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/chain-integrations/with-tron/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ API_PUBLIC_KEY="<Turnkey API Public Key (that starts with 02 or 03)>"
API_PRIVATE_KEY="<Turnkey API Private Key>"
BASE_URL="https://api.turnkey.com"
ORGANIZATION_ID="<Turnkey organization ID>"
TRON_ADDRESS="Set this to the wallet address created from pnpm run createTonWallet"
TRON_WALLET_NAME="Tron wallet"
TRON_ADDRESS="Set this to the wallet address created from pnpm run createTronWallet"
TRON_DELEGATED_SIGNER_ADDRESS="Set this to a second Turnkey Tron address authorized by an active permission"
TRON_PERMISSION_ID="Set this to the active permission ID, typically 2 or greater"
27 changes: 23 additions & 4 deletions examples/chain-integrations/with-tron/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ This example walks through the following:

- Creation of a new Turnkey wallet with a new Tron account
- Obtaining Nile testnet TRX and USDT from a faucet to use for the rest of the examples
- Signing a TRX transaction
- Signing and sending a TRX transaction
- Signing and sending a TRX transaction with a different, permissioned Tron key
- Create a Turnkey policy to parse and guard Tron TRX and TRC-20 transactions

## Getting started
Expand Down Expand Up @@ -52,15 +53,15 @@ Note that this is optional: the script gives you a fresh one if you don't specif

### 3/ Running the scripts

There are 4 scripts that are meant to be run in order that will create a Tron wallet, sign a raw payload, create some policies and sign some transactions abiding by those policies.
The scripts create a Tron wallet, sign a raw payload, sign and send transactions directly and with a delegated Tron signer, create policies, and sign transactions that abide by those policies.

#### createTronWallet

Start with the creating a Tron wallet script: `pnpm run createTronWallet`. This will give you a fresh Tron wallet secured by Turnkey. It will output a wallet address. Note: if this script is run multiple times it will fail because of a duplicate wallet name, you can change the wallet name in the code, or delete it from your dashboard at [app.turnkey.com/dashboard/wallets](https://app.turnkey.com/dashboard/wallets).
Start with the creating a Tron wallet script: `pnpm run createTronWallet`. This will give you a fresh Tron wallet secured by Turnkey and output its address. Set `TRON_WALLET_NAME` to a unique value before each additional run, such as when creating a second wallet for the delegated signer example.

#### Obtaining testnet tokens

To follow along with the rest of the examples you should fund this wallet with TRX and USDT from this faucet: https://nileex.io/join/getJoinPage. Look for the sections labeled "Get 2000 test coins" and "Get 1000 USDT test tokens".
To follow along with the rest of the examples you should fund this wallet with TRX and USDT from this faucet: https://nileex.io/join/getJoinPage. Look for the sections labeled "Get 1000 test coins" and "Get 1000 USDT test tokens".

You can check your balance and view transactions well make later here: https://nile.tronscan.org/

Expand All @@ -70,6 +71,24 @@ Next you should set the `TRON_ADDRESS` environment variable in your .env.local f

The next example you can run is the `pnpm run signRawPayload`. This example demonstrates a typical SignRawPayload activity with your new Tron address!

#### signTransaction

Next run `pnpm run signTransaction`. This example creates a TRX transfer with TronWeb, signs it with Turnkey's SignTransaction API, and broadcasts the serialized signed transaction returned by Turnkey.

#### signTransactionWithDelegatedSigner

This example exercises Tron active permissions, where the transaction owner and signing key are different addresses. It is distinct from authorizing a non-root Turnkey user to sign with the owner's key.

Use a disposable Nile account for this test. Account permission updates replace the existing permission configuration and currently cost 100 TRX. Create a second Turnkey Tron wallet with a unique `TRON_WALLET_NAME`, set its address as `TRON_DELEGATED_SIGNER_ADDRESS`, fund `TRON_ADDRESS`, review `configureDelegatedSigner.ts`, and run:

```bash
CONFIRM_TRON_PERMISSION_UPDATE=true pnpm run configureDelegatedSigner
```

The setup keeps `TRON_ADDRESS` as the sole owner and installs one transfer-only active permission for `TRON_DELEGATED_SIGNER_ADDRESS`. Wait for the permission update to confirm, query the account to find the assigned active permission ID (normally `2`), and set `TRON_PERMISSION_ID` in `.env.local`.

Then run `pnpm run signTransactionWithDelegatedSigner`.

#### transferTRXPolicy

The following example demonstrates creating policies for guarding `TransferContract` transactions, typically known as TRX transfers. Note: if this script is run multiple times it will fail because of a duplicate policy, you can change the policy name in the code, or delete it from your dashboard at [app.turnkey.com/dashboard/security](https://app.turnkey.com/dashboard/security). Run `pnpm run transferTRXPolicy`
Expand Down
3 changes: 3 additions & 0 deletions examples/chain-integrations/with-tron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"private": true,
"scripts": {
"createTronWallet": "tsx src/createTronWallet.ts",
"configureDelegatedSigner": "tsx src/configureDelegatedSigner.ts",
"signRawPayload": "tsx src/signRawPayload.ts",
"signTransaction": "tsx src/signTransaction.ts",
"signTransactionWithDelegatedSigner": "tsx src/signTransactionWithDelegatedSigner.ts",
"transferTRXPolicy": "tsx src/transferTRXPolicy.ts",
"transferTRC20Policy": "tsx src/transferTRC20Policy.ts",
"clean": "rimraf ./dist ./.cache"
Expand Down
113 changes: 113 additions & 0 deletions examples/chain-integrations/with-tron/src/configureDelegatedSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Turnkey } from "@turnkey/sdk-server";
import { TronWeb } from "tronweb";
import * as dotenv from "dotenv";
import * as path from "path";

// Load environment variables from `.env.local`
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing ${name} in .env.local`);
}

return value;
}

async function main() {
if (process.env.CONFIRM_TRON_PERMISSION_UPDATE !== "true") {
throw new Error(
"This replaces the account's active permissions and costs about 100 TRX. Re-run with CONFIRM_TRON_PERMISSION_UPDATE=true after reviewing the script.",
);
}

const turnkeyClient = new Turnkey({
apiBaseUrl: requiredEnv("BASE_URL"),
apiPrivateKey: requiredEnv("API_PRIVATE_KEY"),
apiPublicKey: requiredEnv("API_PUBLIC_KEY"),
defaultOrganizationId: requiredEnv("ORGANIZATION_ID"),
});

const tronWeb = new TronWeb({
fullHost: "https://nile.trongrid.io/", // Testnet
});

const ownerAddress = requiredEnv("TRON_ADDRESS");
const signerAddress = requiredEnv("TRON_DELEGATED_SIGNER_ADDRESS");
if (ownerAddress === signerAddress) {
throw new Error("The delegated signer must differ from the account owner");
}

const ownerAccount = await tronWeb.trx.getAccount(ownerAddress);
if (ownerAccount.is_witness) {
throw new Error("This example does not support witness accounts");
}

const ownerHex = TronWeb.address.toHex(ownerAddress).toLowerCase();
const currentOwnerKey = ownerAccount.owner_permission?.keys.find(
({ address }) => TronWeb.address.toHex(address).toLowerCase() === ownerHex,
);
if (
!currentOwnerKey ||
currentOwnerKey.weight < ownerAccount.owner_permission.threshold
) {
throw new Error(
"TRON_ADDRESS cannot satisfy the account's current owner permission by itself",
);
}

if (ownerAccount.balance < 100_000_000) {
throw new Error(
"The owner needs at least 100 TRX for the account permission update fee",
);
}

const permissionUpdate =
await tronWeb.transactionBuilder.updateAccountPermissions(
ownerAddress,
{
type: 0,
permission_name: "owner",
threshold: 1,
keys: [{ address: ownerAddress, weight: 1 }],
},
undefined,
{
type: 2,
permission_name: "turnkey-delegated-transfer",
threshold: 1,
// Contract type 1 is TransferContract. The remaining 255 bits are unset.
operations: `02${"00".repeat(31)}`,
keys: [{ address: signerAddress, weight: 1 }],
},
);

console.log("Replacing active permissions on:", ownerAddress);
console.log("Delegated TransferContract signer:", signerAddress);

const signedUpdate = await turnkeyClient.apiClient().signTransaction({
signWith: ownerAddress,
unsignedTransaction: permissionUpdate.raw_data_hex,
type: "TRANSACTION_TYPE_TRON",
});

const result = await tronWeb.trx.sendHexTransaction(
signedUpdate.signedTransaction,
);

if (!result.result) {
throw new Error(`Tron broadcast failed: ${JSON.stringify(result)}`);
}

console.log("Permission update sent! ID:", result.txid);
console.log(
"Wait for confirmation before running the delegated signer example.",
);
console.log("https://nile.tronscan.org/#/transaction/" + result.txid);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
4 changes: 2 additions & 2 deletions examples/chain-integrations/with-tron/src/createTronWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
async function main() {
// Initialize Turnkey client
const turnkeyClient = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiBaseUrl: process.env.BASE_URL!,
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
});

const createTronWalletResult = await turnkeyClient.apiClient().createWallet({
walletName: "Tron wallet",
walletName: process.env.TRON_WALLET_NAME ?? "Tron wallet",
accounts: [
{
curve: "CURVE_SECP256K1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
async function main() {
// Initialize Turnkey client
const turnkeyClient = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiBaseUrl: process.env.BASE_URL!,
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
Expand Down
58 changes: 58 additions & 0 deletions examples/chain-integrations/with-tron/src/signTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Turnkey } from "@turnkey/sdk-server";
import { TronWeb } from "tronweb";
import * as dotenv from "dotenv";
import * as path from "path";

// Load environment variables from `.env.local`
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

async function main() {
// Initialize Turnkey client
const turnkeyClient = new Turnkey({
apiBaseUrl: process.env.BASE_URL!,
apiPrivateKey: process.env.API_PRIVATE_KEY!,
apiPublicKey: process.env.API_PUBLIC_KEY!,
defaultOrganizationId: process.env.ORGANIZATION_ID!,
});

// Initialize TronWeb without a private key
const tronWeb = new TronWeb({
fullHost: "https://nile.trongrid.io/", // Testnet
});

const turnkeyAddress = process.env.TRON_ADDRESS!; // Your Tron address in Turnkey
const recipientAddress = "TY1jfzP3s94oSzYECC89EFn17iA8S4imVZ";
const amount = 100; // Amount in SUN (1 TRX = 1,000,000 SUN)

// Create an unsigned transaction
const unsignedTx = await tronWeb.transactionBuilder.sendTrx(
recipientAddress,
amount,
turnkeyAddress,
);

// Sign with Turnkey's SignTransaction API. This returns a fully serialized
// signed Tron transaction.
const signedTx = await turnkeyClient.apiClient().signTransaction({
signWith: turnkeyAddress,
unsignedTransaction: unsignedTx.raw_data_hex,
type: "TRANSACTION_TYPE_TRON",
});

// Broadcast the signed transaction
const result = await tronWeb.trx.sendHexTransaction(
signedTx.signedTransaction,
);

if (!result.result) {
throw new Error(`Tron broadcast failed: ${JSON.stringify(result)}`);
}

console.log("Transaction sent! ID:", result.txid);
console.log("https://nile.tronscan.org/#/transaction/" + result.txid);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Turnkey } from "@turnkey/sdk-server";
import { TronWeb } from "tronweb";
import * as dotenv from "dotenv";
import * as path from "path";

// Load environment variables from `.env.local`
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing ${name} in .env.local`);
}

return value;
}

async function main() {
const turnkeyClient = new Turnkey({
apiBaseUrl: requiredEnv("BASE_URL"),
apiPrivateKey: requiredEnv("API_PRIVATE_KEY"),
apiPublicKey: requiredEnv("API_PUBLIC_KEY"),
defaultOrganizationId: requiredEnv("ORGANIZATION_ID"),
});

const tronWeb = new TronWeb({
fullHost: "https://nile.trongrid.io/", // Testnet
});

const ownerAddress = requiredEnv("TRON_ADDRESS");
const signerAddress = requiredEnv("TRON_DELEGATED_SIGNER_ADDRESS");
const permissionId = Number(requiredEnv("TRON_PERMISSION_ID"));
if (!Number.isInteger(permissionId) || permissionId < 2) {
throw new Error(
"TRON_PERMISSION_ID must be an active permission ID (2 or greater)",
);
}

if (ownerAddress === signerAddress) {
throw new Error(
"The delegated signer must differ from the transaction owner",
);
}

const ownerAccount = await tronWeb.trx.getAccount(ownerAddress);
const permission = ownerAccount.active_permission?.find(
({ id }) => id === permissionId,
);
if (!permission) {
throw new Error(
`Active permission ${permissionId} was not found for ${ownerAddress}`,
);
}

const signerHex = TronWeb.address.toHex(signerAddress).toLowerCase();
const signerKey = permission.keys.find(
({ address }) => TronWeb.address.toHex(address).toLowerCase() === signerHex,
);
if (!signerKey) {
throw new Error(
`${signerAddress} is not authorized by active permission ${permissionId}`,
);
}

if (signerKey.weight < permission.threshold) {
throw new Error(
`The delegated signer weight (${signerKey.weight}) does not meet the permission threshold (${permission.threshold})`,
);
}

const operations = permission.operations ?? "";
const transferContractEnabled =
operations.length >= 2 &&
(Number.parseInt(operations.slice(0, 2), 16) & 2) !== 0;
if (!transferContractEnabled) {
throw new Error(
`Active permission ${permissionId} does not allow TransferContract`,
);
}

const recipientAddress = "TY1jfzP3s94oSzYECC89EFn17iA8S4imVZ";
const amount = 100; // Amount in SUN (1 TRX = 1,000,000 SUN)
const unsignedTx = await tronWeb.transactionBuilder.sendTrx(
recipientAddress,
amount,
ownerAddress,
{ permissionId },
);

console.log("Transaction owner:", ownerAddress);
console.log("Delegated signer:", signerAddress);
console.log("Active permission ID:", permissionId);

const signedTx = await turnkeyClient.apiClient().signTransaction({
signWith: signerAddress,
unsignedTransaction: unsignedTx.raw_data_hex,
type: "TRANSACTION_TYPE_TRON",
});

const result = await tronWeb.trx.sendHexTransaction(
signedTx.signedTransaction,
);

if (!result.result) {
throw new Error(`Tron broadcast failed: ${JSON.stringify(result)}`);
}

console.log("Delegated transaction sent! ID:", result.txid);
console.log("https://nile.tronscan.org/#/transaction/" + result.txid);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Loading