Skip to content

Commit

Permalink
feat(SOLNENG-27): add BuiilderVault web3 provider example
Browse files Browse the repository at this point in the history
  • Loading branch information
MnrGreg committed Nov 14, 2024
1 parent 417a433 commit f3ec616
Show file tree
Hide file tree
Showing 8 changed files with 2,253 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ To get started, click on the links for installation and usage instructions.
- [Stake deposit from Fireblocks wallet with TypeScript SDK](./solana-staking/fireblocks/nodejs/README.md)

## Ethereum Staking
- [Stake deposit from Builder Vault wallet with Web3 Provider TypeScript SDK](./ethereum-staking/buildervault/nodejs-web3provider/README.md)
- [Stake deposit from Builder Vault wallet with Golang SDK](./ethereum-staking/buildervault/golang/README.md)
- [Stake deposit from Builder Vault wallet with TypeScript SDK](./ethereum-staking/buildervault/nodejs/README.md)
- [Stake deposit from Fireblocks wallet with TypeScript SDK](./ethereum-staking/fireblocks/nodejs/README.md) - [(video clip)](https://youtu.be/_6uwwNTh7iQ?feature=shared)
Expand Down
32 changes: 32 additions & 0 deletions ethereum-staking/buildervault/nodejs-web3provider/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
BLOCKDAEMON_STAKE_API_KEY=zpka_...
ETHEREUM_NETWORK=holesky # mainnet | holesky
ETHEREUM_WITHDRAWAL_ADDRESS=0x...

BLOCKDAEMON_RPC_URL="https://svc.blockdaemon.com/native/v1/ethereum/holesky?apiKey=zpka_..."
BUILDERVAULT_PLAYER0_URL="https://tsm-sandbox.prd.wallet.blockdaemon.app:8080"
BUILDERVAULT_PLAYER0_MPCPUBLICKEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtDFBfanInAMHNKKDG2RW/DiSnYeI7scVvfHIwUIRdbPH0gBrsilqxlvsKZTakN8om/Psc6igO+224X8T0J9eMg=="

BUILDERVAULT_PLAYER1_URL="https://tsm-sandbox.prd.wallet.blockdaemon.app:8081"
BUILDERVAULT_PLAYER1_MPCPUBLICKEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqvSkhonTeNhlETse8v3X7g4p100EW9xIqg4aRpD8yDXgB0UYjhd+gFtOCsRT2lRhuqNForqqC+YnBsJeZ4ANxg=="

BUILDERVAULT_PLAYER2_URL="https://tsm-sandbox.prd.wallet.blockdaemon.app:8082"
BUILDERVAULT_PLAYER2_MPCPUBLICKEY="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBaHCIiViexaVaPuER4tE6oJE3IBA0U//GlB51C1kXkT07liVc51uWuYk78wi4e1unxC95QbeIfnDCG2i43fW3g=="

BUILDERVAULT_MASTERKEY_ID="Ap7..."
BUILDERVAULT_ACCOUNT_ID=0
BUILDERVAULT_ADDRESS_INDEX=0

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-----"
BUILDERVAULT_PLAYER0_CLIENT_CERT="./client.crt"
BUILDERVAULT_PLAYER0_CLIENT_KEY="./client.key"
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-----"
BUILDERVAULT_PLAYER1_CLIENT_CERT="./client.crt"
BUILDERVAULT_PLAYER1_CLIENT_KEY="./client.key"
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-----"
BUILDERVAULT_PLAYER2_CLIENT_CERT="./client.crt"
BUILDERVAULT_PLAYER2_CLIENT_KEY="./client.key"

## Optional APIkey-based authentication
# BUILDERVAULT_PLAYER0_APIKEY="apikey0"
# BUILDERVAULT_PLAYER1_APIKEY="apikey1"
# BUILDERVAULT_PLAYER1_APIKEY="apikey2"
8 changes: 8 additions & 0 deletions ethereum-staking/buildervault/nodejs-web3provider/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.crt
*.key
.vscode
init
**/key.txt
**/.env
**/node_modules
/dist/
63 changes: 63 additions & 0 deletions ethereum-staking/buildervault/nodejs-web3provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

# TypeScript Ethereum staking with BuilderVault wallet

```mermaid
sequenceDiagram
autonumber
participant StakeClient as Sample stake<br> client application
participant StakeAPI as Stake Intent API
participant RPC as Blockdaemon RPC
box Builder Vault
participant TSM1 as MPC Wallet <br>(private key share 1)
participant TSM2 as MPC Wallet <br>(private key share 2)
end
StakeClient ->> StakeAPI: get StakeIntent unsigned tx data <br>(amount, withdrawal & recipient address)
StakeClient ->> StakeClient: decode calldata with<br>deposit contract ABI
critical BuilderVault Web3 Provider
StakeClient ->> StakeClient: init web3 provider
StakeClient ->> StakeClient: invoke web3 smart contract deposit<br> function<br>(amount, calldata, contract address)
StakeClient ->> TSM1: partial sign
StakeClient ->> TSM2: partial sign
StakeClient ->> StakeClient: combine partial signatures
StakeClient ->> RPC: broadcast contract execution
end
```

### Prerequisites
- [Node.js](https://nodejs.org/en/download/package-manager) or launch in [code-spaces](https://codespaces.new/Blockdaemon/demo-buildervault-stakingAPI?quickstart=1)
- Register for a demo Builder Vault tenant: https://www.blockdaemon.com/get-started/builder-vault-sandbox-registration
- Download SDK bundle provided in registration email (extract authentication certificates)
- 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.
- Place Builder Vault authentication certificate key-pair `client.crt` & `client.key` in this nodejs folder
- 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
- 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

### Step 1. Set environment variables in .env
```shell
cd ethereum-staking/buildervault/nodejs-web3provider/
cp .env.example .env
```
- update .env with API keys and BuilderVault Vault details

### Step 2. Install package dependancies
```shell
npm config set @sepior:registry=https://gitlab.com/api/v4/projects/56306653/packages/npm/ # Builder Vault nodejsSDK public repository
npm config set @blockdaemon:registry=https://npm.pkg.github.com/
npm install @blockdaemon/buildervault-web3-provider
```

### Step 3. Launch ethereum-stake-bv.ts to determine the BuilderVault wallet address
```shell
ts-node ethereum-stake-bv.ts
```
- if needed, copy the new Ethereum wallet address and fund the account with https://holesky-faucet.pk910.de/#/

### Step 4. Re-launch ethereum-stake-bv.ts to generate the Stake Intent request, execute the contract with BuilderVault, and broadcast the transaction
```shell
ts-node ethereum-stake-bv.ts
```
- observe the confirmed transaction through the generated blockexplorer link
142 changes: 142 additions & 0 deletions ethereum-staking/buildervault/nodejs-web3provider/ethereum-stake-bv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import Web3 from "web3";
import 'dotenv/config'
import { BuildervaultWeb3Provider } from "@blockdaemon/buildervault-web3-provider";


type CreateStakeIntentRequest = {
stakes: {
fee_recipient: string;
withdrawal_address: string;
amount: string;
}[];
};

type CreateStakeIntentResponse = {
stake_intent_id: string;
ethereum: {
stakes: {
stake_id: string;
amount: string;
validator_public_key: string;
withdrawal_credentials: string;
}[];
contract_address: string;
unsigned_transaction: string;
};
};


function createStakeIntent(
bossApiKey: string,
request: CreateStakeIntentRequest,
): Promise<CreateStakeIntentResponse> {

// * Create a stake intent with the Staking Integration API: https://docs.blockdaemon.com/reference/postethereumstakeintent
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-API-Key': bossApiKey,
//'Idempotency-Key': 'FB81DEED-D58B-4948-B51D-99E2E1064B9C',
},
body: JSON.stringify(request),
};

return fetch(
`https://svc.blockdaemon.com/boss/v1/ethereum/${process.env.ETHEREUM_NETWORK}/stake-intents`,
requestOptions,
).then(response => response.json() as Promise<CreateStakeIntentResponse>);
}

async function main() {

const gwei = 10n ** 9n;

// Check for the required environment variables
if (!process.env.BLOCKDAEMON_STAKE_API_KEY) {
throw new Error('BLOCKDAEMON_STAKE_API_KEY environment variable not set');
}

if (!process.env.ETHEREUM_NETWORK) {
throw new Error('ETHEREUM_NETWORK environment variable not set.');
}

if (!process.env.ETHEREUM_WITHDRAWAL_ADDRESS) {
throw new Error('ETHEREUM_WITHDRAWAL_ADDRESS environment variable not set');
}

const eip1193Provider = new BuildervaultWeb3Provider({
rpcUrl: process.env.BLOCKDAEMON_RPC_URL,
playerCount: Number(process.env.BUILDERVAULT_PLAYER_COUNT),

player0Url: process.env.BUILDERVAULT_PLAYER0_URL,
player0MPCpublicKey: process.env.BUILDERVAULT_PLAYER0_MPCPUBLICKEY,

player1Url: process.env.BUILDERVAULT_PLAYER1_URL,
player1MPCpublicKey: process.env.BUILDERVAULT_PLAYER1_MPCPUBLICKEY,

player2Url: process.env.BUILDERVAULT_PLAYER2_URL,
player2MPCpublicKey: process.env.BUILDERVAULT_PLAYER2_MPCPUBLICKEY,

player0ClientCert: process.env.BUILDERVAULT_PLAYER0_CLIENT_CERT,
player0ClientKey: process.env.BUILDERVAULT_PLAYER0_CLIENT_KEY,
player0mTLSpublicKey: process.env.BUILDERVAULT_PLAYER0_MTLSPUBLICKEY,

player1ClientCert: process.env.BUILDERVAULT_PLAYER1_CLIENT_CERT,
player1ClientKey: process.env.BUILDERVAULT_PLAYER1_CLIENT_KEY,
player1mTLSpublicKey: process.env.BUILDERVAULT_PLAYER1_MTLSPUBLICKEY,

player2ClientCert: process.env.BUILDERVAULT_PLAYER2_CLIENT_CERT,
player2ClientKey: process.env.BUILDERVAULT_PLAYER2_CLIENT_KEY,
player2mTLSpublicKey: process.env.BUILDERVAULT_PLAYER2_MTLSPUBLICKEY,

masterKeyId: process.env.BUILDERVAULT_MASTERKEY_ID,
accountId: Number(process.env.BUILDERVAULT_ACCOUNT_ID),
addressIndex: Number(process.env.BUILDERVAULT_ADDRESS_INDEX),
logRequestsAndResponses: false // Verbose logging
})

const web3 = new Web3(eip1193Provider);

const addresses = await web3.eth.getAccounts();
const address = addresses[0];
console.log("Ethereum addresses:", address);
console.log("Initial balance:", await web3.eth.getBalance(address));

const response = await createStakeIntent(process.env.BLOCKDAEMON_STAKE_API_KEY, {
stakes: [
{
amount: '32000000000',
withdrawal_address: process.env.ETHEREUM_WITHDRAWAL_ADDRESS,
fee_recipient: process.env.ETHEREUM_WITHDRAWAL_ADDRESS,
},
],
});

const { unsigned_transaction, contract_address, stakes } = response.ethereum;
const totalDepositAmount = stakes.reduce((sum, next) => sum + BigInt(next.amount), 0n) * gwei;

// Blockdaemon batch deposit smart contract ABI
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"}]
const contract = new web3.eth.Contract(ABI, contract_address);

// Strip batchDeposit methodID
const data = unsigned_transaction.split("0x592c0b7d")[1];
const inputData = web3.eth.abi.decodeParameters(["uint256", "bytes"], data);

// Invoke batchDeposit method
const txid = await contract.methods.batchDeposit(inputData[0], inputData[1]).send({
from: address,
value: totalDepositAmount.toString(10),
});

console.log(`Broadcasted transaction hash: https://${process.env.ETHEREUM_NETWORK}.etherscan.io/tx/${txid.transactionHash}`);
}

main()
.then(() => process.exit(0))
.catch(err => {
console.error(err);
process.exit(1);
});
Loading

0 comments on commit f3ec616

Please sign in to comment.