Skip to content

Commit cfdc2a1

Browse files
committed
cosmos reference
Added FB cosmos reference
1 parent 5b3640f commit cfdc2a1

File tree

6 files changed

+1250
-0
lines changed

6 files changed

+1250
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Blockdaemon Stake
2+
BLOCKDAEMON_STAKE_API_KEY= #staking API key
3+
BLOCKDAEMON_API_KEY= #free ubiquity key
4+
NEAR_NETWORK=testnet # mainnet | Fireblocks testnet = preprod.
5+
PLAN_ID= # Optional. If provided, will use a specific validator plan.
6+
7+
# Fireblocks
8+
FIREBLOCKS_BASE_PATH="https://api.fireblocks.io/v1"
9+
FIREBLOCKS_API_KEY="my-api-key"
10+
FIREBLOCKS_SECRET_KEY="/Users/johndoe/my-secret-key"
11+
FIREBLOCKS_VAULT_ACCOUNT_ID="0"
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
# TypeScript Cosmos 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 cosmos-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 example.ts to generate the Stake Intent request, sign the request with Fireblocks and broadcast the transaction
24+
```shell
25+
npm run start example.ts
26+
```
27+
- [optional] view the signed transaction contents with inspector: https://atomscan.com/
28+
- observe the confirmed transaction through the generated blockexplorer link
29+
30+
31+
This leverages the following operation here at Blockdaemon. You will need a key for our staking API as well as one for the [Blockdaemon API suite](https://docs.blockdaemon.com/docs/quick-start-blockdaemon-api-suite)
32+
- https://docs.blockdaemon.com/reference/cosmos-api-overview
33+
- https://docs.blockdaemon.com/reference/txcompileandsend-txapi
+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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
12+
} from "@fireblocks/ts-sdk";
13+
import { fromHex, toBase64 } from '@cosmjs/encoding';
14+
15+
// Define types for better type checking and readability
16+
export type NewStakeIntentCosmos = {
17+
public_key: PublicKey
18+
amount: string
19+
delegator_address: string
20+
}
21+
22+
export type NewStakeIntentResponse = {
23+
cosmos: {
24+
amount: string
25+
delegator_address: string
26+
hex_transaction: transaction
27+
unsigned_transaction: string
28+
validator_address: string
29+
}
30+
network: string
31+
protocol: string
32+
stake_intent_id: string
33+
customer_id: string
34+
}
35+
36+
type transaction = {
37+
transaction_hash: string,
38+
unsigned_transaction_hex: string
39+
}
40+
41+
type PublicKey = {
42+
type: string
43+
value: string
44+
}
45+
46+
type AccountInformation = {
47+
address: string,
48+
pub_key: {
49+
'@type': string,
50+
key: string
51+
},
52+
account_number: string,
53+
sequence: string
54+
}
55+
56+
// Check if all required environment variables are set
57+
function checkRequiredEnvVars() {
58+
const requiredVars = [
59+
'BLOCKDAEMON_STAKE_API_KEY', 'FIREBLOCKS_BASE_PATH',
60+
'FIREBLOCKS_API_KEY', 'FIREBLOCKS_SECRET_KEY', 'FIREBLOCKS_VAULT_ACCOUNT_ID', 'BLOCKDAEMON_API_KEY'
61+
];
62+
63+
requiredVars.forEach(varName => {
64+
if (!process.env[varName]) {
65+
throw new Error(`${varName} environment variable not set`);
66+
}
67+
});
68+
}
69+
70+
// Determine the configuration for mainnet or testnet
71+
function getConfigForNetwork() {
72+
return {
73+
assetID: "ATOM_COS",
74+
stakeApiUrl: 'https://svc.blockdaemon.com/boss/v1/cosmos/mainnet/stake-intents',
75+
cosmosAccountApiUrl: 'https://svc.blockdaemon.com/cosmos/mainnet/native/cosmos-rest/cosmos/auth/v1beta1/accounts',
76+
compileAndSendUrl: 'https://svc.blockdaemon.com/tx/v1/cosmos-mainnet/compile_and_send'
77+
};
78+
}
79+
80+
async function main() {
81+
checkRequiredEnvVars(); // Check for required environment variables
82+
const config = getConfigForNetwork();
83+
84+
const fireblocks = new Fireblocks({
85+
apiKey: process.env.FIREBLOCKS_API_KEY,
86+
basePath: process.env.FIREBLOCKS_BASE_PATH,
87+
secretKey: readFileSync(process.env.FIREBLOCKS_SECRET_KEY!, "utf8"),
88+
});
89+
90+
// Fetch the Cosmos vault account address from Fireblocks
91+
const vaultAccounts = await fireblocks.vaults.getVaultAccountAssetAddressesPaginated({
92+
vaultAccountId: process.env.FIREBLOCKS_VAULT_ACCOUNT_ID!,
93+
assetId: config.assetID
94+
});
95+
const delegatorAddress = vaultAccounts.data?.addresses?.[0]?.address;
96+
if (!delegatorAddress) {
97+
throw new Error(`Cosmos address not found (vault id: ${process.env.FIREBLOCKS_VAULT_ACCOUNT_ID})`);
98+
}
99+
console.log(`Cosmos address: ${delegatorAddress}\n`);
100+
101+
// Query RPC to get public key address for request and sequence
102+
const accountInformation = await getAccountInformation(config.cosmosAccountApiUrl, delegatorAddress)
103+
console.log('Sequence:', accountInformation.sequence);
104+
105+
// Create a stake intent with the Blockdaemon API for Cosmos
106+
const response = await createStakeIntent(process.env.BLOCKDAEMON_STAKE_API_KEY!, {
107+
public_key: {
108+
type: 'secp256k1',
109+
value: accountInformation.pub_key.key
110+
},
111+
amount: "1000000", //Minimum amount
112+
delegator_address: delegatorAddress
113+
}, config.stakeApiUrl);
114+
115+
// Check if Cosmos-specific property exists
116+
if (!response.cosmos) {
117+
throw "Missing property `cosmos` in Blockdaemon response";
118+
}
119+
120+
console.log(response)
121+
122+
// Sign the transaction via Fireblocks
123+
const signedMessage = await signTx(response.cosmos.hex_transaction.unsigned_transaction_hex, response.cosmos.hex_transaction.transaction_hash, fireblocks, process.env.FIREBLOCKS_VAULT_ACCOUNT_ID!, config.assetID);
124+
if (!signedMessage) {
125+
throw new Error('Failed to sign transaction');
126+
}
127+
128+
const hexSignature = fromHex(signedMessage.signature)
129+
const base64Signature = toBase64(hexSignature)
130+
131+
await broadcastSignedTransaction(response.cosmos.unsigned_transaction, base64Signature, accountInformation.pub_key.key, parseInt(accountInformation.sequence), config.compileAndSendUrl);
132+
}
133+
134+
// Function for creating a stake intent with the Blockdaemon API for Cosmos
135+
async function createStakeIntent(
136+
bossApiKey: string,
137+
request: NewStakeIntentCosmos,
138+
stakeApiUrl: string
139+
): Promise<NewStakeIntentResponse> {
140+
const requestOptions = {
141+
method: 'POST',
142+
headers: {
143+
'Content-Type': 'application/json',
144+
Accept: 'application/json',
145+
'X-API-Key': bossApiKey,
146+
},
147+
body: JSON.stringify(request),
148+
};
149+
150+
const response = await fetch(stakeApiUrl, requestOptions);
151+
if (response.status !== 200) {
152+
throw await response.json();
153+
}
154+
return await response.json() as NewStakeIntentResponse;
155+
}
156+
157+
// Function to sign the transaction via Fireblocks
158+
const signTx = async (
159+
unsignedHex: string,
160+
transactionHex: string,
161+
fireblocks: Fireblocks,
162+
vaultAccount: string,
163+
assetID: string,
164+
): Promise<{ signature: string; pubKey: string }> => {
165+
166+
console.log('transactionHex', transactionHex)
167+
console.log('unsignedHex', unsignedHex)
168+
169+
// Fireblocks signing payload
170+
const transactionPayload: TransactionRequest = {
171+
assetId: assetID,
172+
operation: TransactionOperation.Raw,
173+
source: {
174+
type: TransferPeerPathType.VaultAccount,
175+
id: vaultAccount,
176+
},
177+
extraParameters: {
178+
rawMessageData: {
179+
// Send the hash and preHash content
180+
messages: [
181+
{
182+
content: transactionHex,
183+
preHash: {
184+
content: unsignedHex,
185+
hashAlgorithm: "SHA256",
186+
},
187+
},
188+
],
189+
},
190+
},
191+
};
192+
193+
const transactionResponse: FireblocksResponse<CreateTransactionResponse> =
194+
await fireblocks.transactions.createTransaction({
195+
transactionRequest: transactionPayload,
196+
});
197+
198+
const txId = transactionResponse.data.id;
199+
if (!txId) throw new Error("Transaction ID is undefined.");
200+
201+
const txInfo = await getTxStatus(txId, fireblocks);
202+
203+
const signature = txInfo.signedMessages?.[0]?.signature?.fullSig;
204+
const pubKey = txInfo.signedMessages?.[0].publicKey!;
205+
if (!signature) throw new Error("Missing signature");
206+
207+
return { signature, pubKey };
208+
};
209+
210+
// Helper function to get the transaction status from Fireblocks
211+
const getTxStatus = async (
212+
txId: string,
213+
fireblocks: Fireblocks): Promise<TransactionResponse> => {
214+
let response: FireblocksResponse<TransactionResponse> = await fireblocks.transactions.getTransaction({ txId });
215+
let tx: TransactionResponse = response.data;
216+
while (tx.status !== TransactionStateEnum.Completed) {
217+
await new Promise((resolve) => setTimeout(resolve, 3000));
218+
response = await fireblocks.transactions.getTransaction({ txId });
219+
tx = response.data;
220+
}
221+
return tx;
222+
};
223+
224+
// Broadcast signed transaction
225+
async function broadcastSignedTransaction(unsignedTx: string, signature: string, publicKeyHex: string, sequence: number, compileAndSendUrl: string) {
226+
227+
const requestBody = JSON.stringify({
228+
signatures: [{
229+
sequence: sequence,
230+
sign_mode: 1,
231+
signature: signature,
232+
public_key: publicKeyHex
233+
}],
234+
unsigned_tx: unsignedTx,
235+
});
236+
237+
console.log('Request body:', requestBody);
238+
239+
const requestOptions = {
240+
method: 'POST',
241+
headers: {
242+
'Content-Type': 'application/json',
243+
'Authorization': `Bearer ${process.env.BLOCKDAEMON_API_KEY}`,
244+
},
245+
body: requestBody,
246+
};
247+
248+
const response = await fetch(compileAndSendUrl, requestOptions);
249+
const responseData = await response.json();
250+
if (response.status !== 200) {
251+
throw new Error(`Error: ${JSON.stringify(responseData)}`);
252+
}
253+
254+
console.log('Transaction sent successfully:', responseData);
255+
return responseData;
256+
}
257+
258+
// Function to get account information from Blockdaemon API
259+
async function getAccountInformation(url: string, address: string): Promise<AccountInformation> {
260+
const requestOptions = {
261+
method: 'GET',
262+
headers: {
263+
'Content-Type': 'application/json',
264+
'Authorization': `Bearer ${process.env.BLOCKDAEMON_API_KEY}`,
265+
},
266+
};
267+
268+
const mergedUrl = `${url}/${address}`
269+
270+
const response = await fetch(mergedUrl, requestOptions);
271+
const responseData = await response.json();
272+
if (response.status !== 200) {
273+
throw new Error(`Error: ${JSON.stringify(responseData)}`);
274+
}
275+
276+
console.log('Get Account information:', responseData);
277+
return responseData.account;
278+
}
279+
280+
// Execute the main function
281+
main()
282+
.then(() => process.exit(0))
283+
.catch(err => {
284+
console.error(err);
285+
process.exit(1);
286+
});

0 commit comments

Comments
 (0)