Skip to content

Commit 417a433

Browse files
authored
Merge pull request #2 from Blockdaemon/cardano
cardano fireblocks reference
2 parents 26eb827 + 13fecbd commit 417a433

File tree

7 files changed

+1148
-1
lines changed

7 files changed

+1148
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
init
55
**/key.txt
66
**/.env
7-
**/node_modules
7+
**/node_modules
8+
*/.idea
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Blockdaemon Stake
2+
BLOCKDAEMON_STAKE_API_KEY=
3+
CARDANO_NETWORK=preprod # mainnet | Fireblocks testnet = preprod.
4+
PLAN_ID= # Optional. If provided, will use a specific validator plan.
5+
6+
# Fireblocks
7+
FIREBLOCKS_BASE_PATH="https://api.fireblocks.io/v1"
8+
FIREBLOCKS_API_KEY="my-api-key"
9+
FIREBLOCKS_SECRET_KEY="/Users/johndoe/my-secret-key"
10+
FIREBLOCKS_VAULT_ACCOUNT_ID="0"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
# TypeScript Cardano staking with Fireblocks wallet
3+
4+
5+
### Prerequisites
6+
- [Node.js](https://nodejs.org/en/download/package-manager) or launch in [code-spaces](https://codespaces.new/Blockdaemon/demo-buildervault-stakingAPI?quickstart=1)
7+
- Create Fireblocks [API and Secret key](https://developers.fireblocks.com/docs/manage-api-keys) for use with the [Fireblocks TypeScript SDK](https://github.com/fireblocks/ts-sdk)
8+
- Register free Blockdaemon [Staking API key](https://docs.blockdaemon.com/reference/get-started-staking-api#step-1-sign-up-for-an-api-key) and set in .env as BLOCKDAEMON_STAKE_API_KEY
9+
10+
11+
### Step 1. Set environment variables in .env
12+
```shell
13+
cd cardano-staking/fireblocks/nodejs/
14+
cp .env.example .env
15+
```
16+
- update .env with API keys, Fireblocks Vault ID
17+
18+
### Step 2. Install package dependancies
19+
```shell
20+
npm install
21+
```
22+
23+
### Step 3. Launch cardano-stake-fb.ts to generate the Stake Intent request, sign the request with Fireblocks and broadcast the transaction
24+
```shell
25+
npm run start cardano-stake-fb.ts
26+
```
27+
- [optional] view the signed transaction contents with inspector: https://preprod.cardanoscan.io/
28+
- observe the confirmed transaction through the generated blockexplorer link
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import {readFileSync} from 'fs';
2+
import 'dotenv/config';
3+
import {
4+
Fireblocks,
5+
FireblocksResponse,
6+
TransferPeerPathType,
7+
TransactionRequest,
8+
TransactionResponse,
9+
TransactionOperation,
10+
TransactionStateEnum,
11+
CreateTransactionResponse, SignedMessage
12+
} from "@fireblocks/ts-sdk";
13+
import {
14+
Ed25519Signature, hash_transaction, PublicKey,
15+
Transaction,
16+
TransactionBody,
17+
TransactionWitnessSet, Vkey, Vkeywitness,
18+
Vkeywitnesses
19+
} from "@emurgo/cardano-serialization-lib-nodejs";
20+
21+
// Define the types for Cardano Stake Intent
22+
export type NewStakeIntentCardano = {
23+
base_address: string;
24+
};
25+
26+
async function main() {
27+
28+
// Check for the required environment variables
29+
if (!process.env.BLOCKDAEMON_STAKE_API_KEY) {
30+
throw new Error('BLOCKDAEMON_STAKE_API_KEY environment variable not set');
31+
}
32+
33+
if (!process.env.CARDANO_NETWORK) {
34+
throw new Error('CARDANO_NETWORK environment variable not set.');
35+
}
36+
37+
if (!process.env.FIREBLOCKS_BASE_PATH) {
38+
throw new Error('FIREBLOCKS_BASE_PATH environment variable not set');
39+
}
40+
41+
if (!process.env.FIREBLOCKS_API_KEY) {
42+
throw new Error('FIREBLOCKS_API_KEY environment variable not set');
43+
}
44+
45+
if (!process.env.FIREBLOCKS_SECRET_KEY) {
46+
throw new Error('FIREBLOCKS_SECRET_KEY environment variable not set');
47+
}
48+
49+
if (!process.env.FIREBLOCKS_VAULT_ACCOUNT_ID) {
50+
throw new Error('FIREBLOCKS_VAULT_ACCOUNT_ID environment variable not set');
51+
}
52+
53+
// Determine Fireblocks Asset ID for Cardano
54+
const assetID = "ADA_TEST"; // Use "ADA" for mainnet
55+
56+
// Create a Fireblocks API instance
57+
const fireblocks = new Fireblocks({
58+
apiKey: process.env.FIREBLOCKS_API_KEY,
59+
basePath: process.env.FIREBLOCKS_BASE_PATH,
60+
secretKey: readFileSync(process.env.FIREBLOCKS_SECRET_KEY, "utf8"),
61+
});
62+
63+
// Fetch the Cardano vault account address from Fireblocks
64+
const vaultAccounts = await fireblocks.vaults.getVaultAccountAssetAddressesPaginated({
65+
vaultAccountId: process.env.FIREBLOCKS_VAULT_ACCOUNT_ID,
66+
assetId: assetID
67+
});
68+
const delegatorAddress = vaultAccounts.data?.addresses?.[0]?.address;
69+
if (!delegatorAddress) {
70+
throw new Error(`Cardano address not found (vault id: ${process.env.FIREBLOCKS_VAULT_ACCOUNT_ID})`);
71+
}
72+
console.log(`Cardano address: ${delegatorAddress}\n`);
73+
74+
// Create a stake intent with the Blockdaemon API for Cardano
75+
const response = await createStakeIntent(process.env.BLOCKDAEMON_STAKE_API_KEY, {
76+
base_address: delegatorAddress,
77+
});
78+
79+
// Check if Cardano-specific property exists
80+
if (!response.cardano) {
81+
throw "Missing property `cardano` in Blockdaemon response";
82+
}
83+
84+
// Get the unsigned transaction data returned by the Staking Integration API
85+
const unsignedTransactionHex = response.cardano.unsigned_transaction;
86+
const unsignedTransactionBody = decodeUnsignedTransactionBody(unsignedTransactionHex);
87+
88+
// **Hash the transaction body**
89+
const txHash = hash_transaction(unsignedTransactionBody).to_hex();
90+
console.log(`Transaction Hash: ${txHash}`);
91+
92+
// // Sign the transaction via Fireblocks
93+
const signedMessages = await signTx(txHash, fireblocks, process.env.FIREBLOCKS_VAULT_ACCOUNT_ID, assetID);
94+
if (!signedMessages) {
95+
throw new Error('Failed to sign transaction');
96+
}
97+
98+
// Now we create the signed transaction with both signatures
99+
const signedTransaction = createSignedTransaction(unsignedTransactionBody, signedMessages);
100+
console.log(`Signed transaction (CBOR): ${Buffer.from(signedTransaction).toString('hex')}`);
101+
102+
103+
// Then take the result above and publish it here - https://docs.blockdaemon.com/reference/submittransaction
104+
}
105+
106+
// Function for creating a stake intent with the Blockdaemon API for Cardano
107+
async function createStakeIntent(
108+
bossApiKey: string,
109+
request: NewStakeIntentCardano,
110+
): Promise<any> {
111+
const requestOptions = {
112+
method: 'POST',
113+
headers: {
114+
'Content-Type': 'application/json',
115+
Accept: 'application/json',
116+
'X-API-Key': bossApiKey,
117+
},
118+
body: JSON.stringify(request),
119+
};
120+
121+
const response = await fetch(
122+
`https://svc.blockdaemon.com/boss/v1/cardano/${process.env.CARDANO_NETWORK}/stake-intents`,
123+
requestOptions
124+
);
125+
if (response.status != 200) {
126+
throw await response.json();
127+
}
128+
return await response.json();
129+
}
130+
131+
// Function to sign the transaction via Fireblocks
132+
const signTx = async (
133+
unsignedTransaction: string,
134+
fireblocks: Fireblocks,
135+
vaultAccount: string,
136+
assetID: string,
137+
): Promise<{ publicKey: string; signature: string }[] | undefined> => {
138+
const transactionPayload: TransactionRequest = {
139+
assetId: assetID,
140+
operation: TransactionOperation.Raw,
141+
source: {
142+
type: TransferPeerPathType.VaultAccount,
143+
id: vaultAccount,
144+
},
145+
note: '',
146+
extraParameters: {
147+
rawMessageData: {
148+
messages: [
149+
{
150+
content: unsignedTransaction // The unsigned transaction in hex format
151+
},
152+
{
153+
content: unsignedTransaction,
154+
bip44change: 2
155+
},
156+
],
157+
},
158+
},
159+
};
160+
161+
try {
162+
// Create the transaction and get response
163+
const transactionResponse: FireblocksResponse<CreateTransactionResponse> = await fireblocks.transactions.createTransaction({
164+
transactionRequest: transactionPayload,
165+
});
166+
167+
const txId = transactionResponse.data.id;
168+
if (!txId) {
169+
throw new Error("Transaction ID is undefined.");
170+
}
171+
172+
// Wait for transaction completion and get full tx info with signed messages
173+
const txInfo = await getTxStatus(txId, fireblocks);
174+
175+
console.log(JSON.stringify(txInfo, null, 2));
176+
177+
// Map over signedMessages to extract the publicKey and signature
178+
return txInfo.signedMessages?.map((msg: SignedMessage) => ({
179+
publicKey: msg.publicKey as string,
180+
signature: msg.signature?.fullSig as string,
181+
}));
182+
} catch (error) {
183+
console.error("Error signing transaction:", error);
184+
return undefined;
185+
}
186+
};
187+
188+
// Helper function to get the transaction status from Fireblocks
189+
const getTxStatus = async (
190+
txId: string,
191+
fireblocks: Fireblocks): Promise<TransactionResponse> => {
192+
try {
193+
let response: FireblocksResponse<TransactionResponse> =
194+
await fireblocks.transactions.getTransaction({txId});
195+
let tx: TransactionResponse = response.data;
196+
let messageToConsole: string = `Transaction ${tx.id} is currently at status - ${tx.status}`;
197+
198+
console.log(messageToConsole);
199+
while (tx.status !== TransactionStateEnum.Completed) {
200+
await new Promise((resolve) => setTimeout(resolve, 3000));
201+
202+
response = await fireblocks.transactions.getTransaction({txId});
203+
tx = response.data;
204+
205+
switch (tx.status) {
206+
case TransactionStateEnum.Blocked:
207+
case TransactionStateEnum.Cancelled:
208+
case TransactionStateEnum.Failed:
209+
case TransactionStateEnum.Rejected:
210+
throw new Error(
211+
`Signing request failed/blocked/cancelled: Transaction: ${tx.id} status is ${tx.status}`,
212+
);
213+
default:
214+
console.log(messageToConsole);
215+
break;
216+
}
217+
}
218+
return tx;
219+
} catch (error) {
220+
throw error;
221+
}
222+
};
223+
224+
const decodeUnsignedTransactionBody = (unsignedTransactionHex: string): TransactionBody => {
225+
const unsignedTransactionBuffer = Buffer.from(unsignedTransactionHex, 'hex');
226+
return TransactionBody.from_bytes(unsignedTransactionBuffer);
227+
}
228+
229+
// Function to create the signed transaction with the signatures and public keys
230+
const createSignedTransaction = (unsignedTxBody: TransactionBody, signedMessages: {
231+
publicKey: string,
232+
signature: string
233+
}[]): Uint8Array => {
234+
const witnessSet = TransactionWitnessSet.new();
235+
const vkeyWitnesses = Vkeywitnesses.new();
236+
237+
signedMessages.forEach(({publicKey, signature}) => {
238+
const vkeyWitness = Vkeywitness.new(
239+
Vkey.new(PublicKey.from_bytes(Buffer.from(publicKey, 'hex'))),
240+
Ed25519Signature.from_bytes(Buffer.from(signature, 'hex'))
241+
);
242+
vkeyWitnesses.add(vkeyWitness);
243+
});
244+
245+
witnessSet.set_vkeys(vkeyWitnesses);
246+
const signedTransaction = Transaction.new(unsignedTxBody, witnessSet);
247+
248+
return signedTransaction.to_bytes();
249+
};
250+
251+
main()
252+
.then(() => process.exit(0))
253+
.catch(err => {
254+
console.error(err);
255+
process.exit(1);
256+
});

0 commit comments

Comments
 (0)