Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TON example #402

Merged
merged 6 commits into from
Nov 14, 2024
Merged
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
8 changes: 8 additions & 0 deletions examples/with-ton/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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>"
TON_ADDRESS="<existing TON address in your organization>"
TON_PUBLIC_KEY="<existing TON pubkey compressed matching your above address in your organization>"
TON_RPC_URL="<RPC url for TON, mainnet is https://toncenter.com/api/v2/jsonRPC>"
TON_API_KEY="<API Key corresponding with the above RPC>"
60 changes: 60 additions & 0 deletions examples/with-ton/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Example: `with-ton`

This is a simple example that walks through the construction of a TON transaction and sending the funds out on mainnet

## Getting started

### 1/ Cloning the example

Make sure you have `Node.js` installed locally; we recommend using Node v18+.

```bash
$ git clone https://github.com/tkhq/sdk
$ cd sdk/
$ corepack enable # Install `pnpm`
$ pnpm install -r # Install dependencies
$ pnpm run build-all # Compile source code
$ cd examples/with-ton/
```

### 2/ Setting up Turnkey

The first step is to set up your Turnkey organization. By following the [Quickstart](https://docs.turnkey.com/getting-started/quickstart) guide, you should have:

- A public/private API key pair for Turnkey
- An organization ID

Once you've gathered these values, add them to a new `.env.local` file. Notice that your private key should be securely managed and **_never_** be committed to git.

```bash
$ cp .env.local.example .env.local
```

Now open `.env.local` and add the missing environment variables:

- `API_PUBLIC_KEY`
- `API_PRIVATE_KEY`
- `BASE_URL`
- `ORGANIZATION_ID`
- `TON_ADDRESS`
- `TON_PUBLIC_KEY`
- `TON_RPC_URL`
- `TON_API_KEY`

### 3/ Running the script

Note that this example is currently set up with TON mainnet. You will need a balance to run this example

```bash
$ pnpm start
```

You should see output similar to the following:

```
? Recipient address: (<recipient_ton_address>)

Sending 0.015 TON to <recipient_ton_address>

Transaction sent successfully
```
27 changes: 27 additions & 0 deletions examples/with-ton/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@turnkey/example-with-ton",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "pnpm tsx src/index.ts",
"clean": "rimraf ./dist ./.cache",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@inquirer/prompts": "^2.1.0",
"@noble/hashes": "1.4.0",
"@ton/core": "^0.59.0",
"@ton/crypto": "^3.3.0",
"@ton/ton": "^15.1.0",
"@turnkey/http": "workspace:*",
"@turnkey/sdk-server": "workspace:*",
"async-retry": "^1.3.3",
"cross-fetch": "^4.0.0",
"dotenv": "^16.0.3"
},
"devDependencies": {
"@types/async-retry": "^1.4.8",
"tsx": "^3.12.7",
"typescript": "^5.0.4"
}
}
180 changes: 180 additions & 0 deletions examples/with-ton/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import * as dotenv from "dotenv";
import * as path from "path";
import {
TonClient,
Address,
beginCell,
WalletContractV4,
internal,
storeMessageRelaxed,
SendMode,
Cell,
storeMessage,
external,
} from "@ton/ton";
import { input } from "@inquirer/prompts";
import { Turnkey } from "@turnkey/sdk-server";
import { bytesToHex } from "@noble/hashes/utils";

dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

async function createWalletTransferV4WithTurnkey(args: {
seqno: number;
sendMode: number;
walletId: number;
messages: any[];
turnkeyClient: Turnkey;
walletAddress: string;
}) {
// Check number of messages
if (args.messages.length > 4) {
throw Error("Maximum number of messages in a single transfer is 4");
}

let signingMessageBuilder = beginCell().storeUint(args.walletId, 32);

if (args.seqno === 0) {
for (let i = 0; i < 32; i++) {
signingMessageBuilder.storeBit(1); // Initial state for uninitialized wallet
}
} else {
signingMessageBuilder.storeUint(Math.floor(Date.now() / 1e3) + 60, 32); // Default timeout: 60 seconds
}

signingMessageBuilder.storeUint(args.seqno, 32);
signingMessageBuilder.storeUint(0, 8); // Simple order
for (let m of args.messages) {
signingMessageBuilder.storeUint(args.sendMode, 8);
signingMessageBuilder.storeRef(beginCell().store(storeMessageRelaxed(m)));
}

const signingMessage = signingMessageBuilder.endCell().hash();

// Sign message using Turnkey
const txSignResult = await args.turnkeyClient.apiClient().signRawPayload({
signWith: args.walletAddress,
payload: bytesToHex(signingMessage),
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE",
});

const { r, s } = txSignResult;
const signatureBytes = Buffer.from(r + s, "hex");
const body = beginCell()
.storeBuffer(signatureBytes)
.storeBuilder(signingMessageBuilder)
.endCell();
return body;
}

async function externalTransaction(
client: TonClient,
address: Address,
init: { code: Cell | null; data: Cell | null } | null,
body: Cell
) {
// Check if the contract needs initialization (init code/data)
let neededInit: { code: Cell | null; data: Cell | null } | null = null;
if (init && !(await client.isContractDeployed(address))) {
neededInit = init;
}

// Create the external message
const ext = external({
to: address,
init: neededInit ? { code: neededInit.code, data: neededInit.data } : null,
body: body,
});

// Build the final message to send
const boc = beginCell().store(storeMessage(ext)).endCell().toBoc();

// Send the transaction
await client.sendFile(boc);
}

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

const client = new TonClient({
endpoint: process.env.TON_RPC_URL!,
apiKey: process.env.TON_API_KEY!,
});
const walletAddress = process.env.TON_ADDRESS!;
const walletPublicKey = process.env.TON_PUBLIC_KEY!;

if (!walletAddress || !walletPublicKey) {
throw new Error(
"Please set your TON_ADDRESS and TON_PUBLIC_KEY in the .env.local file."
);
}

console.log(`Using TON address: ${walletAddress}`);

const tonAddress = Address.parse(walletAddress);
let accountData;
try {
accountData = await client.getBalance(tonAddress);
} catch (error) {
throw new Error(
`Failed to retrieve balance for address ${tonAddress}: ${error}`
);
}
if (!accountData || BigInt(accountData) === 0n) {
console.log(
`Your account does not exist or has zero balance. Fund your address ${walletAddress} to proceed.`
);
process.exit(1);
}

const recipientAddress = await input({
message: "Recipient address:",
default: "<recipient_ton_address>",
});

console.log(`\nSending 0.015 TON to ${recipientAddress}`);

const tonWallet = WalletContractV4.create({
workchain: 0,
publicKey: Buffer.from(walletPublicKey, "hex"),
});

const opened = client.open(tonWallet);
const seqno = await opened.getSeqno();
const message = internal({
value: "0.015",
to: recipientAddress,
body: "Transfer body",
});

const body = await createWalletTransferV4WithTurnkey({
seqno,
sendMode: SendMode.PAY_GAS_SEPARATELY,
walletId: tonWallet.walletId,
messages: [message],
turnkeyClient,
walletAddress,
});

// Check if the wallet is deployed, if not provide init data
const init =
opened.init && !(await client.isContractDeployed(tonAddress))
? opened.init
: null;

// Send the transaction using the external transaction logic
externalTransaction(client, tonAddress, init, body);

console.log("Transaction sent successfully.");
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
8 changes: 8 additions & 0 deletions examples/with-ton/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./.cache/.tsbuildinfo"
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.json"]
}
Loading
Loading