Skip to content
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
2 changes: 1 addition & 1 deletion examples/oauth/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ API_PUBLIC_KEY="<Turnkey API Public Key (that starts with 02 or 03)>"
API_PRIVATE_KEY="<Turnkey API Private Key>"
NEXT_PUBLIC_ORGANIZATION_ID="<Turnkey organization ID>"
NEXT_PUBLIC_GOOGLE_CLIENT_ID="<Google OIDC client ID>"
NEXT_PUBLIC_BASE_URL="https://api.turnkey.com"
NEXT_PUBLIC_BASE_URL="https://api.turnkey.com"
16 changes: 13 additions & 3 deletions examples/with-yield-xyz/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ TURNKEY_API_PUBLIC_KEY="<Turnkey API Public Key of the root user (that starts wi
TURNKEY_API_PRIVATE_KEY="<Turnkey API Private Key of the root user>"
TURNKEY_ORGANIZATION_ID="<Turnkey organization ID>"
TURNKEY_BASE_URL="https://api.turnkey.com"
SIGN_WITH= "<Turnkey organization wallet Base address 0x...>"

NONROOT_USER_ID="<userId of the non-root user>"
NONROOT_API_PUBLIC_KEY="<API Public Key of the non-root user (that starts with 02 or 03)>"
NONROOT_API_PRIVATE_KEY="<API Private Key of the non-root user>"


SIGN_WITH="<Turnkey organization wallet Base address 0x...>"
RPC_URL="<RPC URL for Base mainnet>"

YIELD_ID="base-usdc-gtusdcf-0x236919f11ff9ea9550a4287696c2fc9e18e6e890-4626-vault"
YIELD_API_KEY="<Yield.xyz project API Key>"
RPC_URL="<RPC URL for Base mainnet>"
YIELD_API_KEY="<Yield project API Key>"

USDC_ADDRESS="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
gtUSDCf_VAULT_ADDRESS="0x236919F11ff9eA9550A4287696C2FC9e18E6e890"
30 changes: 25 additions & 5 deletions examples/with-yield-xyz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ This example shows how to sign transactions to Yield.xyz vaults on Base Mainnet
- `balance.ts` is checking your balance in any yield and fetches current yield stats.
- `exit.ts` is withdrawing an amount from the yield.

On top of it we showcase the power of the Turnkey policy engine by allowing a non-root Turnkey user to sign only the specific transactions required to interact with Yield.xyz’s Base USDC vault (which internally supplies to Morpho):

- `createPolicies.ts` uses an organization root user (RootQuorum) to create precise policy conditions for a non-root user, restricting their signing permissions to:
- the USDC contract (`USDC_ADDRESS`), and
- Yield.xyz’s Base USDC vault (`gtUSDCf_VAULT_ADDRESS`), which corresponds to Yield.xyz’s identifier for Morpho’s Base USDC vault (`base-usdc-gtusdcf-0x236919f11ff9ea9550a4287696c2fc9e18e6e890-4626-vault`).
- Each policy uses the `eth.tx.data` field to identify which smart contract function is being called. The first four bytes of this field represent the function selector. For the `approve` function, the selector is `0x095ea7b3`; for deposit it is `0x6e553f65`; and for `withdraw`, it’s `0xba087652`. This allows the policies to precisely restrict the non-root user to only those permitted contract calls.

## Getting started

### 1/ Cloning the example
Expand All @@ -30,7 +37,9 @@ The first step is to set up your Turnkey organization and account. By following
- A root user with a public/private API key pair within the Turnkey parent organization
- An organization ID

Make sure you have a [wallet](https://app.turnkey.com/dashboard/wallets) with an Ethereum wallet account created within this organization and have it funded with some ETH and USDC on Base Mainnet.
The next step is to create another user within the organization with a different API key and remove it from the root quorum. You can do this from the Turnkey [dashboard](https://app.turnkey.com/dashboard/security/updateRootQuorum) or [API](https://docs.turnkey.com/api-reference/activities/update-root-quorum). Here's a simple [script](https://github.com/tkhq/sdk/blob/main/examples/kitchen-sink/src/sdk-server/updateRootQuorum.ts) that shows how to update the root quorum using `@turnkey/sdk-server`.

Finally, make sure you have a [wallet](https://app.turnkey.com/dashboard/wallets) with an Ethereum wallet account created within this organization and have it funded with some ETH and USDC on Base Mainnet.

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.

Expand All @@ -44,30 +53,41 @@ Now open `.env.local` and add the missing environment variables:
- `TURNKEY_API_PRIVATE_KEY`
- `TURNKEY_ORGANIZATION_ID`
- `TURNKEY_BASE_URL`
- `NONROOT_USER_ID`
- `NONROOT_API_PUBLIC_KEY`
- `NONROOT_API_PRIVATE_KEY`
- `SIGN_WITH`
- `YIELD_ID`
- `YIELD_API_KEY`
- `RPC_URL`
- `USDC_ADDRESS`
- `gtUSDCf_VAULT_ADDRESS`

### 3/ Setting up the policies for the non-root user

```bash
pnpm createPolicies
```

### 3/ Discover a yield (with metadata)
### 4/ Discover a yield (with metadata)

```bash
pnpm discover
```

### 4/ Enter the yield (deposit via Yield.xyz)
### 5/ Enter the yield (deposit via Yield.xyz)

```bash
pnpm enter
```

### 5/ Check user balance
### 6/ Check user balance

```bash
pnpm balance
```

### 6/ Exit the yield (withdraw funds)
### 7/ Exit the yield (withdraw funds)

```bash
pnpm exit
Expand Down
4 changes: 2 additions & 2 deletions examples/with-yield-xyz/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": "true",
"scripts": {
"build": "pnpm -w run build-all",
"createPolicies": "tsx src/createPolicies.ts",
"enter": "tsx src/enter.ts",
"exit": "tsx src/exit.ts",
"balance": "tsx src/balance.ts",
Expand All @@ -12,9 +13,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@turnkey/api-key-stamper": "workspace:*",
"@turnkey/ethers": "workspace:*",
"@turnkey/http": "workspace:*",
"@turnkey/sdk-server": "workspace:*",
"dotenv": "^16.0.3",
"ethers": "^6.10.0"
}
Expand Down
7 changes: 1 addition & 6 deletions examples/with-yield-xyz/src/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ import * as dotenv from "dotenv";
// Load environment variables from `.env.local`
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

const turnkeyAccount = {
address: process.env.SIGN_WITH!,
};
console.log(turnkeyAccount);

async function main() {
const balanceRes = await fetch(
`https://api.yield.xyz/v1/yields/${process.env.YIELD_ID}/balances`,
Expand All @@ -17,7 +12,7 @@ async function main() {
"Content-Type": "application/json",
"x-api-key": process.env.YIELD_API_KEY!,
},
body: JSON.stringify({ address: turnkeyAccount.address }),
body: JSON.stringify({ address: process.env.SIGN_WITH! }),
},
);
const balances = await balanceRes.json();
Expand Down
98 changes: 98 additions & 0 deletions examples/with-yield-xyz/src/createPolicies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Turnkey } from "@turnkey/sdk-server";
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() {
const turnkeyClient = new Turnkey({
apiBaseUrl: "https://api.turnkey.com",
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
}).apiClient();

// The id of the non-root user that you'll be using to sign the Yield related transactions
const userId = process.env.NONROOT_USER_ID!;

//approval policy
const approvalPolicy = {
policyName:
"Allow API key user to call the approve function on the USDC_ADDRESS",
effect: "EFFECT_ALLOW" as const,
consensus: `approvers.any(user, user.id == '${userId}')`,
condition: `eth.tx.to == '${process.env.USDC_ADDRESS}' && eth.tx.data[0..10] == '0x095ea7b3'`,
notes: "",
};

const { policyId: approvalPolicyId } =
await turnkeyClient.createPolicy(approvalPolicy);

console.log(
[
`Created approval policy:`,
`- Name: ${approvalPolicy.policyName}`,
`- Policy ID: ${approvalPolicyId}`,
`- Effect: ${approvalPolicy.effect}`,
`- Consensus: ${approvalPolicy.consensus}`,
`- Condition: ${approvalPolicy.condition}`,
``,
].join("\n"),
);

//deposit policy
const depositPolicy = {
policyName:
"Allow API key user to call the deposit function on the gtUSDCf_VAULT_ADDRESS",
effect: "EFFECT_ALLOW" as const,
consensus: `approvers.any(user, user.id == '${userId}')`,
condition: `eth.tx.to == '${process.env.gtUSDCf_VAULT_ADDRESS}' && eth.tx.data[0..10] == '0x6e553f65'`,
notes: "",
};

const { policyId: depositPolicyId } =
await turnkeyClient.createPolicy(depositPolicy);

console.log(
[
`Created deposit policy:`,
`- Name: ${depositPolicy.policyName}`,
`- Policy ID: ${depositPolicyId}`,
`- Effect: ${depositPolicy.effect}`,
`- Consensus: ${depositPolicy.consensus}`,
`- Condition: ${depositPolicy.condition}`,
``,
].join("\n"),
);

//withdraw policy
const withdrawPolicy = {
policyName:
"Allow API key user to call the withdraw function on the gtUSDCf_VAULT_ADDRESS",
effect: "EFFECT_ALLOW" as const,
consensus: `approvers.any(user, user.id == '${userId}')`,
condition: `eth.tx.to == '${process.env.gtUSDCf_VAULT_ADDRESS}' && eth.tx.data[0..10] == '0xba087652'`,
notes: "",
};

const { policyId: withdrawPolicyId } =
await turnkeyClient.createPolicy(withdrawPolicy);

console.log(
[
`Created withdraw policy:`,
`- Name: ${withdrawPolicy.policyName}`,
`- Policy ID: ${withdrawPolicyId}`,
`- Effect: ${withdrawPolicy.effect}`,
`- Consensus: ${withdrawPolicy.consensus}`,
`- Condition: ${withdrawPolicy.condition}`,
``,
].join("\n"),
);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
30 changes: 13 additions & 17 deletions examples/with-yield-xyz/src/enter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,25 @@ import * as path from "path";
import * as dotenv from "dotenv";
import { ethers } from "ethers";
import { TurnkeySigner } from "@turnkey/ethers";
import { TurnkeyClient } from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";

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

async function main() {
// Initialize Turnkey client and signer
const turnkeyClient = new TurnkeyClient(
{ baseUrl: process.env.TURNKEY_BASE_URL! },
new ApiKeyStamper({
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
}),
);

const turnkeyAccount = {
address: process.env.SIGN_WITH!,
};
// Initialize the Turnkey client
const turnkeyClient = new TurnkeyServerSDK({
apiBaseUrl: "https://api.turnkey.com",
apiPublicKey: process.env.NONROOT_API_PUBLIC_KEY!,
apiPrivateKey: process.env.NONROOT_API_PRIVATE_KEY!,
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
});

// Initialize the Turnkey Signer
const turnkeySigner = new TurnkeySigner({
client: turnkeyClient,
client: turnkeyClient.apiClient(),
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
signWith: turnkeyAccount.address,
signWith: process.env.SIGN_WITH!,
});

const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!);
Expand All @@ -35,7 +30,7 @@ async function main() {
const depositAmount = "0.5";
const enterPayload = {
yieldId: process.env.YIELD_ID!, // e.g."base-usdc-gtusdcf-0x236919f11ff9ea9550a4287696c2fc9e18e6e890-4626-vault"
address: turnkeyAccount.address,
address: process.env.SIGN_WITH!,
arguments: { amount: depositAmount },
};

Expand All @@ -48,6 +43,7 @@ async function main() {
body: JSON.stringify(enterPayload),
});
const action = await enterRes.json();
console.log("Yield API response:", JSON.stringify(action, null, 2));

// Sign and broadcast each transaction step
for (const tx of action.transactions) {
Expand Down
35 changes: 15 additions & 20 deletions examples/with-yield-xyz/src/exit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,35 @@ import * as path from "path";
import * as dotenv from "dotenv";
import { ethers } from "ethers";
import { TurnkeySigner } from "@turnkey/ethers";
import { TurnkeyClient } from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";

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

async function main() {
// Initialize Turnkey client and signer
const turnkeyClient = new TurnkeyClient(
{ baseUrl: process.env.TURNKEY_BASE_URL! },
new ApiKeyStamper({
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
}),
);

// Replace with your wallet address or fetched wallet from Turnkey
const turnkeyAccount = {
address: process.env.SIGN_WITH!,
};
// Initialize the Turnkey client
const turnkeyClient = new TurnkeyServerSDK({
apiBaseUrl: "https://api.turnkey.com",
apiPublicKey: process.env.NONROOT_API_PUBLIC_KEY!,
apiPrivateKey: process.env.NONROOT_API_PRIVATE_KEY!,
defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
});

// Initialize the Turnkey Signer
const turnkeySigner = new TurnkeySigner({
client: turnkeyClient,
client: turnkeyClient.apiClient(),
organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
signWith: turnkeyAccount.address,
signWith: process.env.SIGN_WITH!,
});

const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!);
const connectedSigner = turnkeySigner.connect(provider);
const exitPayload = {
yieldId: process.env.YIELD_ID!,
address: turnkeyAccount.address,
yieldId: process.env.YIELD_ID,
address: process.env.SIGN_WITH!,
arguments: { amount: "0.1" },
};

// Prepare withdrawal via Yield.xyz
const exitRes = await fetch("https://api.yield.xyz/v1/actions/exit", {
method: "POST",
headers: {
Expand All @@ -46,6 +40,7 @@ async function main() {
body: JSON.stringify(exitPayload),
});
const exitAction = await exitRes.json();
console.log("Yield API response:", JSON.stringify(exitAction, null, 2));

for (const tx of exitAction.transactions) {
const unsignedTx = JSON.parse(tx.unsignedTransaction);
Expand Down
Loading
Loading