Skip to content

Commit f3ec616

Browse files
committed
feat(SOLNENG-27): add BuiilderVault web3 provider example
1 parent 417a433 commit f3ec616

File tree

8 files changed

+2253
-0
lines changed

8 files changed

+2253
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ To get started, click on the links for installation and usage instructions.
88
- [Stake deposit from Fireblocks wallet with TypeScript SDK](./solana-staking/fireblocks/nodejs/README.md)
99

1010
## Ethereum Staking
11+
- [Stake deposit from Builder Vault wallet with Web3 Provider TypeScript SDK](./ethereum-staking/buildervault/nodejs-web3provider/README.md)
1112
- [Stake deposit from Builder Vault wallet with Golang SDK](./ethereum-staking/buildervault/golang/README.md)
1213
- [Stake deposit from Builder Vault wallet with TypeScript SDK](./ethereum-staking/buildervault/nodejs/README.md)
1314
- [Stake deposit from Fireblocks wallet with TypeScript SDK](./ethereum-staking/fireblocks/nodejs/README.md) - [(video clip)](https://youtu.be/_6uwwNTh7iQ?feature=shared)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
BLOCKDAEMON_STAKE_API_KEY=zpka_...
2+
ETHEREUM_NETWORK=holesky # mainnet | holesky
3+
ETHEREUM_WITHDRAWAL_ADDRESS=0x...
4+
5+
BLOCKDAEMON_RPC_URL="https://svc.blockdaemon.com/native/v1/ethereum/holesky?apiKey=zpka_..."
6+
BUILDERVAULT_PLAYER0_URL="https://tsm-sandbox.prd.wallet.blockdaemon.app:8080"
7+
BUILDERVAULT_PLAYER0_MPCPUBLICKEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtDFBfanInAMHNKKDG2RW/DiSnYeI7scVvfHIwUIRdbPH0gBrsilqxlvsKZTakN8om/Psc6igO+224X8T0J9eMg=="
8+
9+
BUILDERVAULT_PLAYER1_URL="https://tsm-sandbox.prd.wallet.blockdaemon.app:8081"
10+
BUILDERVAULT_PLAYER1_MPCPUBLICKEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqvSkhonTeNhlETse8v3X7g4p100EW9xIqg4aRpD8yDXgB0UYjhd+gFtOCsRT2lRhuqNForqqC+YnBsJeZ4ANxg=="
11+
12+
BUILDERVAULT_PLAYER2_URL="https://tsm-sandbox.prd.wallet.blockdaemon.app:8082"
13+
BUILDERVAULT_PLAYER2_MPCPUBLICKEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBaHCIiViexaVaPuER4tE6oJE3IBA0U//GlB51C1kXkT07liVc51uWuYk78wi4e1unxC95QbeIfnDCG2i43fW3g=="
14+
15+
BUILDERVAULT_MASTERKEY_ID="Ap7..."
16+
BUILDERVAULT_ACCOUNT_ID=0
17+
BUILDERVAULT_ADDRESS_INDEX=0
18+
19+
BUILDERVAULT_PLAYER0_MTLSPUBLICKEY="-----BEGIN CERTIFICATE-----\nMIICMTCCAdegAwIBAgICB+MwCgYIKoZIzj0EAwIwgaAxCzAJBgNVBAYTAlVTMRMw\nEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtMb3MgQW5nZWxlczEUMBIGA1UE\nCgwLQmxvY2tkYWVtb24xFDASBgNVBAsMC0Jsb2NrZGFlbW9uMRQwEgYDVQQDDAtC\nbG9ja2RhZW1vbjEkMCIGCSqGSIb3DQEJARYVYWRtaW5AYmxvY2tkYWVtb24uY29t\nMB4XDTI0MDIxMzE3MjE0OFoXDTI5MDIxMzE3MjE0OFowTjELMAkGA1UEBhMCVVMx\nEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMRQwEgYD\nVQQKEwtCbG9ja2RhZW1vbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGlixcUc\nYC0ByeutoHHdi3zxWCg5iPAJcxVLvzBUdD2+XdCWEgS/xwFEef9Tl3xFdfK4iWSQ\nnjmtYMTaHMM6mfWjUjBQMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEF\nBQcDAgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUW6ouasv5oWo7MZ4ZzlE/mpbDrIMw\nCgYIKoZIzj0EAwIDSAAwRQIgSDKHZmsnylzL8kopFSeo8L6LQGxyd/NsBRb+8STI\n1cECIQChi4cl5nJgTXCBzJEHicnRk/0vl+9zq6iABMV+KTXJxA==\n-----END CERTIFICATE-----"
20+
BUILDERVAULT_PLAYER0_CLIENT_CERT="./client.crt"
21+
BUILDERVAULT_PLAYER0_CLIENT_KEY="./client.key"
22+
BUILDERVAULT_PLAYER1_MTLSPUBLICKEY="-----BEGIN CERTIFICATE-----\nMIICMjCCAdegAwIBAgICB+MwCgYIKoZIzj0EAwIwgaAxCzAJBgNVBAYTAlVTMRMw\nEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtMb3MgQW5nZWxlczEUMBIGA1UE\nCgwLQmxvY2tkYWVtb24xFDASBgNVBAsMC0Jsb2NrZGFlbW9uMRQwEgYDVQQDDAtC\nbG9ja2RhZW1vbjEkMCIGCSqGSIb3DQEJARYVYWRtaW5AYmxvY2tkYWVtb24uY29t\nMB4XDTI0MDIxMzE3MjEzMloXDTI5MDIxMzE3MjEzMlowTjELMAkGA1UEBhMCVVMx\nEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMRQwEgYD\nVQQKEwtCbG9ja2RhZW1vbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKz8yGcE\nYIhaQYCA2As30cRIL2rLrB2uKpcFpydE55RoI3Hw+QaeNCfR5znZQZM4bVVquT4i\nxDGhVnQKU5EQU/WjUjBQMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEF\nBQcDAgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUW6ouasv5oWo7MZ4ZzlE/mpbDrIMw\nCgYIKoZIzj0EAwIDSQAwRgIhAO9yXpssqar6IdgmEOIfAsha0ZIWG56nwE8/GbyN\nBiTaAiEAhhEClrSm/TzmWxODXamBz0pmQ9qNFsrtbGsDhLOe8O8=\n-----END CERTIFICATE-----"
23+
BUILDERVAULT_PLAYER1_CLIENT_CERT="./client.crt"
24+
BUILDERVAULT_PLAYER1_CLIENT_KEY="./client.key"
25+
BUILDERVAULT_PLAYER2_MTLSPUBLICKEY="-----BEGIN CERTIFICATE-----\nMIICMTCCAdegAwIBAgICB+MwCgYIKoZIzj0EAwIwgaAxCzAJBgNVBAYTAlVTMRMw\nEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtMb3MgQW5nZWxlczEUMBIGA1UE\nCgwLQmxvY2tkYWVtb24xFDASBgNVBAsMC0Jsb2NrZGFlbW9uMRQwEgYDVQQDDAtC\nbG9ja2RhZW1vbjEkMCIGCSqGSIb3DQEJARYVYWRtaW5AYmxvY2tkYWVtb24uY29t\nMB4XDTI0MDIxMzE3MjEzMVoXDTI5MDIxMzE3MjEzMVowTjELMAkGA1UEBhMCVVMx\nEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMRQwEgYD\nVQQKEwtCbG9ja2RhZW1vbjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMmi8FGO\nAPfRA76hjIwU9kPzN88nZjp4hTmYnJ8Hb5yUzIF8PCwKi3nvtFRSFGW8UT07zdXN\nTkaA/PtsSdFT4JajUjBQMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEF\nBQcDAgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUW6ouasv5oWo7MZ4ZzlE/mpbDrIMw\nCgYIKoZIzj0EAwIDSAAwRQIgew3hpT6O48N2WvccxDyNniAbwNV4NgCw8KXnLgkN\nR/4CIQD0/mV7K8i7HivST09bbSY4zakZVyGQpu+uUUq+6AzvDg==\n-----END CERTIFICATE-----"
26+
BUILDERVAULT_PLAYER2_CLIENT_CERT="./client.crt"
27+
BUILDERVAULT_PLAYER2_CLIENT_KEY="./client.key"
28+
29+
## Optional APIkey-based authentication
30+
# BUILDERVAULT_PLAYER0_APIKEY="apikey0"
31+
# BUILDERVAULT_PLAYER1_APIKEY="apikey1"
32+
# BUILDERVAULT_PLAYER1_APIKEY="apikey2"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.crt
2+
*.key
3+
.vscode
4+
init
5+
**/key.txt
6+
**/.env
7+
**/node_modules
8+
/dist/
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
# TypeScript Ethereum staking with BuilderVault wallet
3+
4+
```mermaid
5+
sequenceDiagram
6+
autonumber
7+
participant StakeClient as Sample stake<br> client application
8+
participant StakeAPI as Stake Intent API
9+
participant RPC as Blockdaemon RPC
10+
11+
box Builder Vault
12+
participant TSM1 as MPC Wallet <br>(private key share 1)
13+
participant TSM2 as MPC Wallet <br>(private key share 2)
14+
end
15+
16+
StakeClient ->> StakeAPI: get StakeIntent unsigned tx data <br>(amount, withdrawal & recipient address)
17+
StakeClient ->> StakeClient: decode calldata with<br>deposit contract ABI
18+
19+
20+
critical BuilderVault Web3 Provider
21+
StakeClient ->> StakeClient: init web3 provider
22+
StakeClient ->> StakeClient: invoke web3 smart contract deposit<br> function<br>(amount, calldata, contract address)
23+
StakeClient ->> TSM1: partial sign
24+
StakeClient ->> TSM2: partial sign
25+
StakeClient ->> StakeClient: combine partial signatures
26+
StakeClient ->> RPC: broadcast contract execution
27+
end
28+
```
29+
30+
### Prerequisites
31+
- [Node.js](https://nodejs.org/en/download/package-manager) or launch in [code-spaces](https://codespaces.new/Blockdaemon/demo-buildervault-stakingAPI?quickstart=1)
32+
- Register for a demo Builder Vault tenant: https://www.blockdaemon.com/get-started/builder-vault-sandbox-registration
33+
- Download SDK bundle provided in registration email (extract authentication certificates)
34+
- Execute the go sample `go run main.go keygen` to generate an ECDSA master key ID across the 3 MPC players of the BuilderVault. This Master Key ID will be used to generate wallet addresses and sign operations.
35+
- Place Builder Vault authentication certificate key-pair `client.crt` & `client.key` in this nodejs folder
36+
- Register for free Blockdaemon [RPC API key](https://docs.blockdaemon.com/reference/get-started-rpc#step-1-sign-up-for-an-api-key) and set in .env as BLOCKDAEMON_API_KEY
37+
- Register a 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
38+
39+
### Step 1. Set environment variables in .env
40+
```shell
41+
cd ethereum-staking/buildervault/nodejs-web3provider/
42+
cp .env.example .env
43+
```
44+
- update .env with API keys and BuilderVault Vault details
45+
46+
### Step 2. Install package dependancies
47+
```shell
48+
npm config set @sepior:registry=https://gitlab.com/api/v4/projects/56306653/packages/npm/ # Builder Vault nodejsSDK public repository
49+
npm config set @blockdaemon:registry=https://npm.pkg.github.com/
50+
npm install @blockdaemon/buildervault-web3-provider
51+
```
52+
53+
### Step 3. Launch ethereum-stake-bv.ts to determine the BuilderVault wallet address
54+
```shell
55+
ts-node ethereum-stake-bv.ts
56+
```
57+
- if needed, copy the new Ethereum wallet address and fund the account with https://holesky-faucet.pk910.de/#/
58+
59+
### Step 4. Re-launch ethereum-stake-bv.ts to generate the Stake Intent request, execute the contract with BuilderVault, and broadcast the transaction
60+
```shell
61+
ts-node ethereum-stake-bv.ts
62+
```
63+
- observe the confirmed transaction through the generated blockexplorer link
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import Web3 from "web3";
2+
import 'dotenv/config'
3+
import { BuildervaultWeb3Provider } from "@blockdaemon/buildervault-web3-provider";
4+
5+
6+
type CreateStakeIntentRequest = {
7+
stakes: {
8+
fee_recipient: string;
9+
withdrawal_address: string;
10+
amount: string;
11+
}[];
12+
};
13+
14+
type CreateStakeIntentResponse = {
15+
stake_intent_id: string;
16+
ethereum: {
17+
stakes: {
18+
stake_id: string;
19+
amount: string;
20+
validator_public_key: string;
21+
withdrawal_credentials: string;
22+
}[];
23+
contract_address: string;
24+
unsigned_transaction: string;
25+
};
26+
};
27+
28+
29+
function createStakeIntent(
30+
bossApiKey: string,
31+
request: CreateStakeIntentRequest,
32+
): Promise<CreateStakeIntentResponse> {
33+
34+
// * Create a stake intent with the Staking Integration API: https://docs.blockdaemon.com/reference/postethereumstakeintent
35+
const requestOptions = {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
Accept: 'application/json',
40+
'X-API-Key': bossApiKey,
41+
//'Idempotency-Key': 'FB81DEED-D58B-4948-B51D-99E2E1064B9C',
42+
},
43+
body: JSON.stringify(request),
44+
};
45+
46+
return fetch(
47+
`https://svc.blockdaemon.com/boss/v1/ethereum/${process.env.ETHEREUM_NETWORK}/stake-intents`,
48+
requestOptions,
49+
).then(response => response.json() as Promise<CreateStakeIntentResponse>);
50+
}
51+
52+
async function main() {
53+
54+
const gwei = 10n ** 9n;
55+
56+
// Check for the required environment variables
57+
if (!process.env.BLOCKDAEMON_STAKE_API_KEY) {
58+
throw new Error('BLOCKDAEMON_STAKE_API_KEY environment variable not set');
59+
}
60+
61+
if (!process.env.ETHEREUM_NETWORK) {
62+
throw new Error('ETHEREUM_NETWORK environment variable not set.');
63+
}
64+
65+
if (!process.env.ETHEREUM_WITHDRAWAL_ADDRESS) {
66+
throw new Error('ETHEREUM_WITHDRAWAL_ADDRESS environment variable not set');
67+
}
68+
69+
const eip1193Provider = new BuildervaultWeb3Provider({
70+
rpcUrl: process.env.BLOCKDAEMON_RPC_URL,
71+
playerCount: Number(process.env.BUILDERVAULT_PLAYER_COUNT),
72+
73+
player0Url: process.env.BUILDERVAULT_PLAYER0_URL,
74+
player0MPCpublicKey: process.env.BUILDERVAULT_PLAYER0_MPCPUBLICKEY,
75+
76+
player1Url: process.env.BUILDERVAULT_PLAYER1_URL,
77+
player1MPCpublicKey: process.env.BUILDERVAULT_PLAYER1_MPCPUBLICKEY,
78+
79+
player2Url: process.env.BUILDERVAULT_PLAYER2_URL,
80+
player2MPCpublicKey: process.env.BUILDERVAULT_PLAYER2_MPCPUBLICKEY,
81+
82+
player0ClientCert: process.env.BUILDERVAULT_PLAYER0_CLIENT_CERT,
83+
player0ClientKey: process.env.BUILDERVAULT_PLAYER0_CLIENT_KEY,
84+
player0mTLSpublicKey: process.env.BUILDERVAULT_PLAYER0_MTLSPUBLICKEY,
85+
86+
player1ClientCert: process.env.BUILDERVAULT_PLAYER1_CLIENT_CERT,
87+
player1ClientKey: process.env.BUILDERVAULT_PLAYER1_CLIENT_KEY,
88+
player1mTLSpublicKey: process.env.BUILDERVAULT_PLAYER1_MTLSPUBLICKEY,
89+
90+
player2ClientCert: process.env.BUILDERVAULT_PLAYER2_CLIENT_CERT,
91+
player2ClientKey: process.env.BUILDERVAULT_PLAYER2_CLIENT_KEY,
92+
player2mTLSpublicKey: process.env.BUILDERVAULT_PLAYER2_MTLSPUBLICKEY,
93+
94+
masterKeyId: process.env.BUILDERVAULT_MASTERKEY_ID,
95+
accountId: Number(process.env.BUILDERVAULT_ACCOUNT_ID),
96+
addressIndex: Number(process.env.BUILDERVAULT_ADDRESS_INDEX),
97+
logRequestsAndResponses: false // Verbose logging
98+
})
99+
100+
const web3 = new Web3(eip1193Provider);
101+
102+
const addresses = await web3.eth.getAccounts();
103+
const address = addresses[0];
104+
console.log("Ethereum addresses:", address);
105+
console.log("Initial balance:", await web3.eth.getBalance(address));
106+
107+
const response = await createStakeIntent(process.env.BLOCKDAEMON_STAKE_API_KEY, {
108+
stakes: [
109+
{
110+
amount: '32000000000',
111+
withdrawal_address: process.env.ETHEREUM_WITHDRAWAL_ADDRESS,
112+
fee_recipient: process.env.ETHEREUM_WITHDRAWAL_ADDRESS,
113+
},
114+
],
115+
});
116+
117+
const { unsigned_transaction, contract_address, stakes } = response.ethereum;
118+
const totalDepositAmount = stakes.reduce((sum, next) => sum + BigInt(next.amount), 0n) * gwei;
119+
120+
// Blockdaemon batch deposit smart contract ABI
121+
const ABI = [{"inputs":[{"internalType":"contract IDepositContract","name":"_depositContract","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"validUntil","type":"uint256"},{"internalType":"bytes","name":"args","type":"bytes"}],"name":"batchDeposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"depositContract","outputs":[{"internalType":"contract IDepositContract","name":"","type":"address"}],"stateMutability":"view","type":"function"}]
122+
const contract = new web3.eth.Contract(ABI, contract_address);
123+
124+
// Strip batchDeposit methodID
125+
const data = unsigned_transaction.split("0x592c0b7d")[1];
126+
const inputData = web3.eth.abi.decodeParameters(["uint256", "bytes"], data);
127+
128+
// Invoke batchDeposit method
129+
const txid = await contract.methods.batchDeposit(inputData[0], inputData[1]).send({
130+
from: address,
131+
value: totalDepositAmount.toString(10),
132+
});
133+
134+
console.log(`Broadcasted transaction hash: https://${process.env.ETHEREUM_NETWORK}.etherscan.io/tx/${txid.transactionHash}`);
135+
}
136+
137+
main()
138+
.then(() => process.exit(0))
139+
.catch(err => {
140+
console.error(err);
141+
process.exit(1);
142+
});

0 commit comments

Comments
 (0)