Skip to content

Commit 86359ec

Browse files
nooxxloicttn
authored andcommitted
rebase
1 parent 9f7756b commit 86359ec

File tree

8 files changed

+911
-23
lines changed

8 files changed

+911
-23
lines changed

examples/ada.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1-
const { WalletServer } = require('cardano-wallet-js');
2-
import { Transaction } from '@stricahq/typhonjs';
1+
import { Kiln } from "../src/kiln";
2+
const fs = require('fs');
33

4+
const apiSecretPath = fs.readFileSync(__dirname + '/fireblocks_secret.key', 'utf8');
45

56
const f = async () => {
67
try {
8+
const k = new Kiln({
9+
testnet: true,
10+
apiToken: 'kiln_dTkxUTFRdHBMZm9vNFFycFhDSTZCdlJsbjJZang5VnY6bVE3bUYyUExZeDd3LUM2Ty01THJ2QTlyMmVtUG92NzI5ejRqU19FVzQ3UFdkUFdZTmgyMHJ2VWcxcUdjWXNsMg',
11+
integrations: [
12+
{
13+
name: 'vault1',
14+
provider: 'fireblocks',
15+
fireblocksApiKey: '53aee35e-04b7-9314-8f28-135a66c8af2c',
16+
fireblocksSecretKeyPath: apiSecretPath,
17+
vaultAccountId: '7'
18+
}
19+
],
20+
});
21+
22+
const tx = await k.ada.craftStakeTx(
23+
'376acfff-e35d-4b7c-90da-c6acb8ea7197',
24+
'addr_test1qpy358g8glafrucevf0rjpmzx2k5esn5uvjh7dzuakpdhv4g2egyt3y3qw6jrguz0lmyhxygjdg2ytaf5z6ueaety7dsmpcee5',
25+
);
26+
27+
const txSigned = await k.ada.sign('vault1', tx);
28+
// const hash = await k.ada.broadcast(txSigned);
29+
// console.log(hash);
730
} catch (err){
831
console.log(err);
932
}

package-lock.json

Lines changed: 557 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@
2727
},
2828
"homepage": "https://github.com/kilnfi/sdk-js#readme",
2929
"dependencies": {
30+
"@blockfrost/blockfrost-js": "^5.0.0",
3031
"@cosmjs/encoding": "^0.28.13",
3132
"@cosmjs/proto-signing": "^0.28.13",
3233
"@cosmjs/stargate": "^0.28.13",
34+
"@emurgo/cardano-serialization-lib-nodejs": "^11.0.5",
3335
"@ethereumjs/common": "^2.6.5",
3436
"@ethereumjs/tx": "^3.5.2",
3537
"@solana/web3.js": "^1.47.3",
3638
"@stricahq/typhonjs": "^1.2.7",
3739
"axios": "^0.23.0",
40+
"bip39": "^3.0.4",
3841
"cardano-wallet-js": "^1.4.0",
3942
"cosmjs-types": "^0.5.0",
4043
"crypto": "^1.0.1",

src/integrations/fb_signer.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
TransactionResponse,
88
} from "fireblocks-sdk";
99

10-
type AssetId = 'SOL_TEST' | 'SOL' | 'ETH_TEST3' | 'ETH' | 'ATOM_COS_TEST' | 'ATOM_COS';
10+
type AssetId = 'SOL_TEST' | 'SOL' | 'ETH_TEST3' | 'ETH' | 'ATOM_COS_TEST' | 'ATOM_COS' | 'ADA_TEST' | 'ADA';
1111

1212
export class FbSigner {
1313
protected fireblocks: FireblocksSDK;
@@ -55,11 +55,7 @@ export class FbSigner {
5555
id: String(this.vaultAccountId)
5656
},
5757
note,
58-
extraParameters: {
59-
rawMessageData: {
60-
messages: payloadToSign
61-
}
62-
}
58+
extraParameters: payloadToSign,
6359

6460
}
6561
);

src/kiln.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Integrations } from "./types/integrations";
55
import { Rpcs } from "./types/rpcs";
66
import { AtomService } from "./services/atom";
77
import { AccountService } from "./services/accounts";
8+
import { AdaService } from "./services/ada";
89

910
type Config = {
1011
apiToken: string;
@@ -18,6 +19,7 @@ export class Kiln {
1819
sol: SolService;
1920
accounts: AccountService;
2021
atom: AtomService;
22+
ada: AdaService;
2123

2224
constructor({ testnet, apiToken, integrations, rpcs }: Config) {
2325
api.defaults.headers.common.Authorization = `Bearer ${apiToken}`;
@@ -27,9 +29,10 @@ export class Kiln {
2729
? 'https://api.testnet.kiln.fi/'
2830
: 'https://api.kiln.fi/';
2931

32+
this.accounts = new AccountService({ testnet });
3033
this.eth = new EthService({ testnet, integrations, rpc: rpcs?.ethereum, });
3134
this.sol = new SolService({ testnet, integrations, rpc: rpcs?.solana, });
3235
this.atom = new AtomService({ testnet, integrations, rpc: rpcs?.atom, });
33-
this.accounts = new AccountService({ testnet });
36+
this.ada = new AdaService({ testnet, integrations });
3437
}
3538
}

src/services/ada.ts

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
2+
import { Transaction } from '@emurgo/cardano-serialization-lib-nodejs';
3+
import { mnemonicToEntropy } from 'bip39';
4+
import { Service } from "./service";
5+
import { InternalAdaConfig, UTXO } from "../types/ada";
6+
import {
7+
BlockFrostAPI,
8+
BlockfrostServerError,
9+
} from "@blockfrost/blockfrost-js";
10+
import { InvalidIntegration } from "../errors/integrations";
11+
import { PeerType, TransactionOperation } from "fireblocks-sdk";
12+
13+
const CARDANO_PARAMS = {
14+
COINS_PER_UTXO_WORD: '34482',
15+
MAX_TX_SIZE: 16384,
16+
MAX_VALUE_SIZE: 5000,
17+
};
18+
19+
export class AdaService extends Service {
20+
private client: BlockFrostAPI;
21+
22+
constructor({ testnet, integrations }: InternalAdaConfig) {
23+
super({ testnet, integrations });
24+
this.client = new BlockFrostAPI({ projectId: 'testnetQMV4zxv1wbnSaqTFWuW3tVVOGA9noUkZ' });
25+
}
26+
27+
/**
28+
* Craft ada delegate transaction
29+
* @param accountId id of the kiln account to use for the stake transaction
30+
* @param walletAddress withdrawal creds /!\ losing it => losing the ability to withdraw
31+
*/
32+
async craftStakeTx(
33+
accountId: string,
34+
walletAddress: string,
35+
): Promise<Transaction> {
36+
37+
let utxo: UTXO = [];
38+
try {
39+
utxo = await this.client.addressesUtxosAll(walletAddress);
40+
} catch (error) {
41+
if (error instanceof BlockfrostServerError && error.status_code === 404) {
42+
// Address derived from the seed was not used yet
43+
// In this case Blockfrost API will return 404
44+
utxo = [];
45+
} else {
46+
throw error;
47+
}
48+
}
49+
50+
if (utxo.length === 0) {
51+
throw new Error(`You should send ADA to ${walletAddress} to have enough funds to sent a transaction`);
52+
}
53+
54+
// Get current blockchain slot from latest block
55+
const latestBlock = await this.client.blocksLatest();
56+
const currentSlot = latestBlock.slot;
57+
if (!currentSlot) {
58+
throw Error('Failed to fetch slot number');
59+
}
60+
61+
return this.composeTransaction(
62+
walletAddress,
63+
'addr_test1qqh2fphcgd0qsmwsqf4v8v9z2w3cpmzw5y9nx6h8z9v85qj7mjg5eydjgyvn3md3fwlyt2e4veynlwutp7u99m4l6q2sp3rdkv',
64+
'1000000',
65+
utxo,
66+
currentSlot,
67+
);
68+
}
69+
70+
/**
71+
* Sign transaction with given integration
72+
* @param integration
73+
* @param transaction
74+
*/
75+
async sign(integration: string, transaction: Transaction): Promise<Transaction> {
76+
const currentIntegration = this.integrations?.find(int => int.name === integration);
77+
if (!currentIntegration) {
78+
throw new InvalidIntegration(`Unknown integration, please provide an integration name that matches one of the integrations provided in the config.`);
79+
}
80+
81+
// We only support fireblocks integration for now
82+
if (currentIntegration.provider !== 'fireblocks') {
83+
throw new InvalidIntegration(`Unsupported integration provider: ${currentIntegration.provider}`);
84+
}
85+
86+
if (!this.fbSigner) {
87+
throw new InvalidIntegration(`Could not retrieve fireblocks signer.`);
88+
}
89+
90+
const message = transaction.to_hex();
91+
92+
const payload = {
93+
rawMessageData: {
94+
messages: [
95+
{
96+
"content": message,
97+
},
98+
]
99+
},
100+
inputsSelection: {
101+
inputsToSpend: JSON.parse(transaction.body().inputs().to_json()),
102+
}
103+
};
104+
105+
const tx = await this.fbSigner.signWithFB(payload, 'ADA_TEST');
106+
const sigBuffer = Buffer.from(tx.signedMessages![0].signature.fullSig, 'hex');
107+
console.log(tx.signedMessages);
108+
// // transaction.set_is_valid(true);
109+
//
110+
// const txHash = CardanoWasm.hash_transaction(transaction.body());
111+
// const witnesses = CardanoWasm.TransactionWitnessSet.new();
112+
// const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
113+
// const MNEMONIC = 'word 1 word 2';
114+
// const bip32PrvKey = this.mnemonicToPrivateKey(MNEMONIC);
115+
// const { signKey } = this.deriveAddressPrvKey(bip32PrvKey, this.testnet);
116+
// vkeyWitnesses.add(CardanoWasm.make_vkey_witness(txHash, signKey));
117+
// witnesses.set_vkeys(vkeyWitnesses);
118+
// console.log(witnesses);
119+
// const tx = CardanoWasm.Transaction.new(transaction.body(), witnesses);
120+
121+
return transaction;
122+
123+
}
124+
125+
/**
126+
* Broadcast transaction to the network
127+
* @param transaction
128+
*/
129+
async broadcast(transaction: Transaction): Promise<string | undefined> {
130+
try {
131+
return await this.client.txSubmit(transaction.to_bytes());
132+
} catch (error) {
133+
// submit could fail if the transactions is rejected by cardano node
134+
if (error instanceof BlockfrostServerError && error.status_code === 400) {
135+
console.log(error.message);
136+
} else {
137+
// rethrow other errors
138+
throw error;
139+
}
140+
}
141+
}
142+
143+
private composeTransaction (
144+
address: string,
145+
outputAddress: string,
146+
outputAmount: string,
147+
utxos: UTXO,
148+
currentSlot: number,
149+
): Transaction {
150+
if (!utxos || utxos.length === 0) {
151+
throw Error(`No utxo on address ${address}`);
152+
}
153+
154+
const txBuilder = CardanoWasm.TransactionBuilder.new(
155+
CardanoWasm.TransactionBuilderConfigBuilder.new()
156+
.fee_algo(
157+
CardanoWasm.LinearFee.new(
158+
CardanoWasm.BigNum.from_str('44'),
159+
CardanoWasm.BigNum.from_str('155381'),
160+
),
161+
)
162+
.pool_deposit(CardanoWasm.BigNum.from_str('500000000'))
163+
.key_deposit(CardanoWasm.BigNum.from_str('2000000'))
164+
.coins_per_utxo_word(
165+
CardanoWasm.BigNum.from_str(CARDANO_PARAMS.COINS_PER_UTXO_WORD),
166+
)
167+
.max_value_size(CARDANO_PARAMS.MAX_VALUE_SIZE)
168+
.max_tx_size(CARDANO_PARAMS.MAX_TX_SIZE)
169+
.build(),
170+
);
171+
172+
const outputAddr = CardanoWasm.Address.from_bech32(outputAddress);
173+
const changeAddr = CardanoWasm.Address.from_bech32(address);
174+
175+
// Set TTL to +2h from currentSlot
176+
// If the transaction is not included in a block before that slot it will be cancelled.
177+
const ttl = currentSlot + 7200;
178+
txBuilder.set_ttl(ttl);
179+
180+
// Add output to the tx
181+
txBuilder.add_output(
182+
CardanoWasm.TransactionOutput.new(
183+
outputAddr,
184+
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(outputAmount)),
185+
),
186+
);
187+
188+
// Filter out multi asset utxo to keep this simple
189+
const lovelaceUtxos = utxos.filter(
190+
(u: any) => !u.amount.find((a: any) => a.unit !== 'lovelace'),
191+
);
192+
193+
// Create TransactionUnspentOutputs from utxos fetched from Blockfrost
194+
const unspentOutputs = CardanoWasm.TransactionUnspentOutputs.new();
195+
for (const utxo of lovelaceUtxos) {
196+
const amount = utxo.amount.find(
197+
(a: any) => a.unit === 'lovelace',
198+
)?.quantity;
199+
200+
if (!amount) continue;
201+
202+
const inputValue = CardanoWasm.Value.new(
203+
CardanoWasm.BigNum.from_str(amount.toString()),
204+
);
205+
206+
const input = CardanoWasm.TransactionInput.new(
207+
CardanoWasm.TransactionHash.from_bytes(Buffer.from(utxo.tx_hash, 'hex')),
208+
utxo.output_index,
209+
);
210+
const output = CardanoWasm.TransactionOutput.new(changeAddr, inputValue);
211+
unspentOutputs.add(CardanoWasm.TransactionUnspentOutput.new(input, output));
212+
}
213+
214+
txBuilder.add_inputs_from(
215+
unspentOutputs,
216+
CardanoWasm.CoinSelectionStrategyCIP2.LargestFirst,
217+
);
218+
219+
// Adds a change output if there are more ADA in utxo than we need for the transaction,
220+
// these coins will be returned to change address
221+
txBuilder.add_change_if_needed(changeAddr);
222+
223+
// Build transaction
224+
return txBuilder.build_tx();
225+
};
226+
227+
private harden (num: number): number {
228+
return 0x80000000 + num;
229+
};
230+
231+
private deriveAddressPrvKey (
232+
bipPrvKey: CardanoWasm.Bip32PrivateKey,
233+
testnet: boolean,
234+
): {
235+
signKey: CardanoWasm.PrivateKey;
236+
address: string;
237+
} {
238+
const networkId = testnet
239+
? CardanoWasm.NetworkInfo.testnet().network_id()
240+
: CardanoWasm.NetworkInfo.mainnet().network_id();
241+
const accountIndex = 0;
242+
const addressIndex = 0;
243+
244+
const accountKey = bipPrvKey
245+
.derive(this.harden(1852)) // purpose
246+
.derive(this.harden(1815)) // coin type
247+
.derive(this.harden(accountIndex)); // account #
248+
249+
const utxoKey = accountKey
250+
.derive(0) // external
251+
.derive(addressIndex);
252+
253+
const stakeKey = accountKey
254+
.derive(2) // chimeric
255+
.derive(0)
256+
.to_public();
257+
258+
const baseAddress = CardanoWasm.BaseAddress.new(
259+
networkId,
260+
CardanoWasm.StakeCredential.from_keyhash(
261+
utxoKey.to_public().to_raw_key().hash(),
262+
),
263+
CardanoWasm.StakeCredential.from_keyhash(stakeKey.to_raw_key().hash()),
264+
);
265+
266+
const address = baseAddress.to_address().to_bech32();
267+
268+
return { signKey: utxoKey.to_raw_key(), address: address };
269+
};
270+
271+
private mnemonicToPrivateKey (
272+
mnemonic: string,
273+
): CardanoWasm.Bip32PrivateKey {
274+
const entropy = mnemonicToEntropy(mnemonic);
275+
276+
const rootKey = CardanoWasm.Bip32PrivateKey.from_bip39_entropy(
277+
Buffer.from(entropy, 'hex'),
278+
Buffer.from(''),
279+
);
280+
281+
return rootKey;
282+
};
283+
}

0 commit comments

Comments
 (0)