Ethereum 스테이킹 튜토리얼
DSRV에서 ETH를 스테이킹하세요.
새로운 검증자 키를 생성하고, 해당 검증자에 ETH를 예치하기 위한 스테이킹 트랜잭션을 생성·서명·전파하는 방식으로 진행됩니다.
- DSRV API를 통해 검증자 키 생성 및 예치용 unsigned 트랜잭션을 요청
- 반환된 트랜잭션을 사용자 지갑에서 직접 서명
- 서명된 트랜잭션을 DSRV를 통해 네트워크에 브로드캐스트
- 트랜잭션 해시를 기준으로 처리 상태 확인
1. 예치 스테이킹 트랜잭션 요청
검증자 키 생성(할당)과 함께, 예치에 필요한 deposit data 및 unsigned 트랜잭션을 요청합니다. 이 단계에서는 실제 서명이나 전송은 발생하지 않으며, 스테이킹에 필요한 트랜잭션 정보만 생성됩니다.
반환되는 unsignedTransaction은 이후 단계에서 지갑 서명에 사용되므로 반드시 저장해두어야 합니다.
요청 본문 예시:
const response = await fetch(
`https://api.dsrv.com/api/v1/staking/ethereum/hoodi/request-deposit`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
pubkeyCount: 1,
ethAmount: 32,
withdrawAddress: WITHDRAW_ADDRESS,
requesterAddress: REQUESTER_ADDRESS,
}),
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Request Deposit Fail: ${response.status} - ${errorText}`);
}
const responseData = await response.json();
// 다음 단계에서 사용할 unsignedTransaction 저장
const unsignedTransaction = responseData.data.result.unsignedTransaction;ethAmount와 pubkeyCount 의미
ethAmount와 pubkeyCount 의미일반적으로 validator 1개당 32 ETH를 예치합니다.
- 여러 validator를 한 번에 예치하려면
pubkeyCount를 늘리세요. ethAmount는 32 ETH로 요청하는 것을 권장합니다.- 총 예치금은 보통
32 ETH × pubkeyCount로 구성됩니다.
- 총 예치금은 보통
응답 예시:
{
"statusCode": 200,
"data": {
"result": {
"requestId": "212d464e-0acd-4e5e-b234-64d12efe9827",
"assigned": true,
"assignedCount": 1,
"message": "Deposit data generated successfully",
"depositData": [
{
"pubkey": "0x81e270729b953b3ab80078d6565bf000bcd2222a50f6d0dcee0b577de73b0e788d2d0ccd4aaa93bff8eb5de243371ad8",
"withdrawalCredentials": "0x010000000000000000000000B9FF5a65980Ad8D4b6955288cee93184CcCb33f1",
"amount": "32000000000",
"signature": "0xabcd1234....",
"depositMessageRoot": "0x1234abcd....",
"depositDataRoot": "0xef012345....",
"forkVersion": "0x10000910",
"networkName": "hoodi"
}
],
"unsignedTransaction": {
"to": "0x.... (네트워크별로 달라질 수 있으므로 응답값 그대로 사용)",
"data": "0x4....",
"value": "0x1bc16d674ec800000",
"network": "hoodi"
}
}
}
}응답 필드 설명
| 필드 | 타입 | 설명 |
|---|---|---|
requestId | string | 요청 추적을 위한 고유 식별자 |
assigned | boolean | pubkey 할당 성공 여부 |
assignedCount | number | 할당된 pubkey 개수 |
message | string | 처리 결과 메시지 |
depositData | array | 예치 데이터 배열 |
depositData[].pubkey | string | 밸리데이터 공개 키 (0x 접두사, 48 bytes) |
depositData[].withdrawalCredentials | string | 출금 자격 증명 (0x01 접두사 + 주소) |
depositData[].amount | string | 예치 금액 (gwei 단위) |
depositData[].signature | string | BLS 서명 |
depositData[].depositMessageRoot | string | 예치 메시지 머클 루트 |
depositData[].depositDataRoot | string | 예치 데이터 머클 루트 |
depositData[].forkVersion | string | 네트워크 포크 버전 |
depositData[].networkName | string | 네트워크 이름 |
unsignedTransaction | object | 서명되지 않은 트랜잭션 객체 |
(중요) 단위/호출 대상 주의
depositData[].amount단위: gwei (32 ETH =32000000000)unsignedTransaction.value단위: weivalue는 16진수(hex) 문자열(0x...)로 반환됩니다.- 예: 32 ETH(wei) =
0x1bc16d674ec800000
- 호출 대상 컨트랙트 주소(
unsignedTransaction.to): 네트워크(및 환경)에 따라 달라질 수 있습니다.- 따라서 고객은 반드시 API가 반환한
unsignedTransaction.to값을 그대로 사용해 트랜잭션을 서명해야 합니다. - (Deposit Contract 주소나 예시 주소를 임의로 넣어 서명하면 실패할 수 있습니다.)
- 따라서 고객은 반드시 API가 반환한
2. 예치 스테이킹 트랜잭션 서명
API가 준 unsignedTransaction을 사용자 지갑에서 직접 서명합니다.
개인 키는 DSRV에 전달되지 않습니다.
아래 예시는 Legacy(Type-0) 서명 방식입니다.
import { privateKeyToAccount } from 'viem/accounts';
import { createWalletClient, createPublicClient, http } from 'viem';
import { hoodi } from 'viem/chains';
const RPC_URL = 'https://rpc.hoodi.ethpandaops.io';
const PRIVATE_KEY = '0xYourWalletPrivateKey';
// 1단계에서 받은 unsignedTransaction을 그대로 사용하세요.
const unsignedTransaction = {
to: '0x... (API 응답값 그대로)',
data: '0x...',
value: '0x1bc16d674ec800000', // wei (hex string)
};
const account = privateKeyToAccount(PRIVATE_KEY);
const publicClient = createPublicClient({
chain: hoodi,
transport: http(RPC_URL),
});
const walletClient = createWalletClient({
account,
chain: hoodi,
transport: http(RPC_URL),
});
// nonce
const nonce = await publicClient.getTransactionCount({ address: account.address });
// gas estimate
const gas = await publicClient.estimateGas({
account: account.address,
to: unsignedTransaction.to,
data: unsignedTransaction.data,
value: BigInt(unsignedTransaction.value),
});
// legacy gasPrice
const gasPrice = await publicClient.getGasPrice();
const signedTransaction = await walletClient.signTransaction({
to: unsignedTransaction.to,
data: unsignedTransaction.data,
value: BigInt(unsignedTransaction.value),
nonce,
gas,
gasPrice,
});
// signedTransaction 예: '0x02f9...'(혹은 '0xf8...' 등)
console.log('signedTransaction:', signedTransaction);추가 - 가스 설정 (옵션: EIP-1559 Type-2)
네트워크 상황에 따라 EIP-1559(Type-2)를 사용해 가스비를 동적으로 설정할 수 있습니다.
// EIP-1559 수수료 추정
const { maxFeePerGas, maxPriorityFeePerGas } =
await publicClient.estimateFeesPerGas();
const signedTransaction = await walletClient.signTransaction({
to: unsignedTransaction.to,
data: unsignedTransaction.data,
value: BigInt(unsignedTransaction.value),
nonce,
gas,
maxFeePerGas,
maxPriorityFeePerGas,
});Gas limit/fee가 부족하면 트랜잭션이 실패(revert 또는 out-of-gas)할 수 있습니다.
3. 예치 트랜잭션 브로드캐스팅
서명된 트랜잭션을 DSRV API를 통해 네트워크에 브로드캐스트합니다. 브로드캐스팅이 완료되면 트랜잭션 해시가 반환됩니다.
const response = await fetch(
`https://api.dsrv.com/api/v1/staking/ethereum/hoodi/broadcast`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
signedTransaction,
}),
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Broadcast Fail: ${response.status} - ${errorText}`);
}
const broadcastData = await response.json();
const txHash = broadcastData.data.txHash;
console.log('txHash:', txHash);응답 예시:
{
"statusCode": 200,
"data": {
"txHash": "0x0d5f7ca....",
"network": "hoodi"
}
}4. 브로드캐스팅 확인
반환된 txHash로 트랜잭션이 블록에 포함되었는지(컨펌 여부) 확인합니다.
상태가 confirmed로 변경되면, ETH 스테이킹 예치가 정상적으로 완료된 것입니다.
const response = await fetch(
`https://api.dsrv.com/api/v1/staking/ethereum/hoodi/transactions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
txHash,
}),
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Transactions Fail: ${response.status} - ${errorText}`);
}
const txData = await response.json();
console.log(txData);응답 예시 (pending 상태):
{
"statusCode": 200,
"data": {
"result": {
"txHash": "0x0d5f7caf178.....",
"network": "hoodi",
"status": "pending",
"message": "Transaction is in mempool but not yet included in a block."
}
}
}응답 예시 (confirmed 상태):
{
"statusCode": 200,
"data": {
"result": {
"txHash": "0x0d5f7caf178.....",
"network": "hoodi",
"status": "confirmed",
"message": "Transaction confirmed in block 12345678",
"receipt": {
"blockNumber": 12345678,
"blockHash": "0x...",
"transactionHash": "0x0d5f7caf178.....",
"status": "success",
"gasUsed": "21000",
...
}
}
}
}트랜잭션 상태값
| 상태 | 설명 |
|---|---|
not_found | 트랜잭션이 mempool에 없거나 삭제됨 |
pending | 트랜잭션이 mempool에 있으나 아직 블록에 포함되지 않음 |
confirmed | 트랜잭션이 블록에 포함되어 확정됨 |
timeout | 트랜잭션 receipt 조회 시간 초과 |
confirmed상태일 때만receipt필드가 포함됩니다.receipt에는 블록 번호, 가스 사용량 등 상세 정보가 담겨 있습니다.
5. 출금 서명 생성
이 단계에서는 출금 요청에 필요한 서명을 생성합니다. 출금(Voluntary Exit)은 되돌릴 수 없는 작업이므로, 본인 확인을 위해 Withdrawal Address의 개인 키로 서명이 필요합니다. 개인 키는 DSRV에 전달되지 않으며, 클라이언트에서 직접 서명됩니다.
import { privateKeyToAccount } from 'viem/accounts';
const PRIVATE_KEY = '0xYourWithdrawalPrivateKey';
const account = privateKeyToAccount(PRIVATE_KEY);
// 서버로 함께 보낼 customText와 정확히 일치해야 합니다.
const customText = 'I confirm withdrawal request';
const withdrawerSign = await account.signMessage({
message: customText,
});
console.log('withdrawerSign:', withdrawerSign);
console.log('withdrawerAddress:', account.address);6. 출금 요청
생성된 서명과 함께 출금 요청을 DSRV API에 전송합니다. API는 Voluntary Exit 메시지를 생성하고 비콘체인에 직접 제출합니다. 요청이 성공하면 밸리데이터는 Exit Queue에 진입하며, 네트워크 상황에 따라 실제 출금까지 시간이 소요될 수 있습니다.
const response = await fetch(
`https://api.dsrv.com/api/v1/staking/ethereum/hoodi/request-withdraw`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: JSON.stringify({
pubkeys: [PUBKEY],
withdrawerSign,
customText,
withdrawerAddress: WITHDRAW_ADDRESS,
}),
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Request Withdraw Fail: ${response.status} - ${errorText}`);
}
const responseData = await response.json();
console.log(responseData);응답 예시:
{
"statusCode": 200,
"data": {
"result": {
"exitData": {
"exit_data": [
{
"message": {
"epoch": "200000",
"validator_index": "12345"
},
"signature": "0xabcd1234...."
}
]
},
"beaconSubmission": {
"successful": [
{
"exitData": {
"message": { "epoch": "200000", "validator_index": "12345" },
"signature": "0xabcd1234...."
},
"success": true
}
],
"failed": [],
"totalRequested": 1,
"totalSuccessful": 1,
"totalFailed": 0
}
}
}
}출금 응답 필드 설명
| 필드 | 타입 | 설명 |
|---|---|---|
exitData.exit_data | array | 생성된 Voluntary Exit 데이터 배열 |
exitData.exit_data[].message.epoch | string | Exit이 처리될 epoch |
exitData.exit_data[].message.validator_index | string | 밸리데이터 인덱스 |
exitData.exit_data[].signature | string | Exit 메시지 서명 |
beaconSubmission.successful | array | 비콘체인 제출 성공 목록 |
beaconSubmission.failed | array | 비콘체인 제출 실패 목록 |
beaconSubmission.totalRequested | number | 총 요청된 Exit 수 |
beaconSubmission.totalSuccessful | number | 성공한 Exit 수 |
beaconSubmission.totalFailed | number | 실패한 Exit 수 |
각 단계별 요청·응답 스펙과 추가 옵션에 대한 상세 내용은 API Reference 문서를 참고해주세요.
Updated 20 days ago
