Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/vault-demo/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Stage 1: build the plugin
FROM golang:1.21-alpine AS plugin-builder
RUN apk add --no-cache git make
RUN git clone https://github.com/pelipas/vault-plugin-secp256k1.git /build
WORKDIR /build
RUN CGO_ENABLED=0 go build -o secpsign .

FROM openbao/openbao:latest

USER root

RUN apk add --no-cache jq

RUN mkdir -p /opt/openbao/plugins \
&& mkdir -p /opt/openbao/scripts \
&& mkdir -p /vault/keys \
&& mkdir -p /etc/openbao

COPY --from=plugin-builder /build/secpsign /opt/openbao/plugins/secpsign
COPY init-vault.sh /opt/openbao/scripts/init-vault.sh
COPY openbao.hcl /etc/openbao/openbao.hcl

RUN chmod +x /opt/openbao/plugins/secpsign \
&& chmod +x /opt/openbao/scripts/init-vault.sh

ENV BAO_ADDR=http://127.0.0.1:8200

ENTRYPOINT ["/bin/sh", "-c", "/opt/openbao/scripts/init-vault.sh & exec bao server -config=/etc/openbao/openbao.hcl"]
148 changes: 148 additions & 0 deletions src/vault-demo/demo.vault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import 'dotenv/config'

import { ethers } from 'ethers'
import { createSignerFromEnv } from './signer-factory.js'

/**
* Force Vault mode for this demo
*/
process.env.SIGNER_TYPE = 'vault'

const RPC_URL = process.env.ETHEREUM_RPC_URL

if (!RPC_URL) {
throw new Error('ETHEREUM_RPC_URL is required')
}

const CHAIN_ID = Number(process.env.CHAIN_ID ?? '11155111')

// EURC on Sepolia
const EURC_ADDRESS = '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'

function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}

async function main(): Promise<void> {
console.log('=== VAULT SIGNER + OPENBAO DEMO ===\n')

/**
* STEP 1: Create provider + signer via factory
*/
console.log('Step 1: Creating Vault signer via factory...')

const provider = new ethers.JsonRpcProvider(RPC_URL, {
name: 'sepolia',
chainId: CHAIN_ID,
})

const signer = createSignerFromEnv(provider, CHAIN_ID)

const address = await signer.getAddress()

console.log(` Address (from OpenBao): ${address}`)
console.log(
` instanceof AbstractSigner: ${signer instanceof ethers.AbstractSigner}`,
)

/**
* STEP 2: (Optional) sign message
* NOTE: VaultSigner does not support EIP-191 with Kaleido ethsign
*/
console.log('\nStep 2: Signing message...')

try {
const message = 'Vault OpenBao test message'
const signature = await signer.signMessage(message)

console.log(` Signature: ${signature}`)

const recovered = ethers.verifyMessage(message, signature)

console.log(` Recovered: ${recovered}`)
console.log(
` Valid: ${recovered.toLowerCase() === address.toLowerCase()}`,
)
} catch (err) {
console.log(
` Sign message not supported: ${getErrorMessage(err)}`,
)
}

/**
* STEP 3: Load ocean.js
*/
console.log('\nStep 3: Loading ocean.js...')

const { ConfigHelper, Datatoken } = await import('@oceanprotocol/lib')

const oceanConfig = new ConfigHelper().getConfig(CHAIN_ID)

console.log(` Ocean config loaded for chain ${CHAIN_ID}`)

/**
* STEP 4: Create Datatoken instance
*/
console.log('\nStep 4: Creating Datatoken instance...')

const datatoken = new Datatoken(signer, CHAIN_ID)

console.log(' Datatoken ready with VaultSigner')

/**
* STEP 5: Check EURC balance
*/
console.log('\nStep 5: Checking EURC balance...')

const eurcBalance = await datatoken.balance(EURC_ADDRESS, address)

console.log(` EURC Balance: ${eurcBalance}`)

/**
* STEP 6: Approve EURC for Ocean FRE (signed inside OpenBao)
*/
const spenderAddress = oceanConfig.fixedRateExchangeAddress as string

console.log('\nStep 6: Approving EURC (Vault signed tx)...')
console.log(` Token: ${EURC_ADDRESS}`)
console.log(` Spender: ${spenderAddress}`)
console.log(` Amount: 100`)

try {
const approveTx = await datatoken.approve(
EURC_ADDRESS,
spenderAddress,
'100',
)

console.log(` Approve tx: ${JSON.stringify(approveTx)}`)
console.log(' Approval successful')

/**
* STEP 7: Verify allowance
*/
console.log('\nStep 7: Checking allowance...')

const allowance = await datatoken.allowance(
EURC_ADDRESS,
address,
spenderAddress,
)

console.log(` Allowance: ${allowance}`)
} catch (err) {
const message = getErrorMessage(err)

console.error(` Approve failed: ${message}`)

if (message.includes('insufficient funds')) {
console.log(` Fund wallet with ETH: ${address}`)
}
}

console.log('\n=== VAULT DEMO COMPLETE ===')
}

main().catch((error: unknown) => {
console.error('Vault demo failed:', getErrorMessage(error))
})
125 changes: 125 additions & 0 deletions src/vault-demo/init-vault.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/bin/sh
# TO BE FIXED
set -e

export BAO_ADDR=http://127.0.0.1:8200
export BAO_TOKEN=root

echo "[init] Waiting for OpenBao..."

until bao status >/dev/null 2>&1; do
sleep 2
done

# ----------------------------
# INIT (only if not initialized)
# ----------------------------

STATUS=$(bao status -format=json 2>/dev/null || true)
INITIALIZED=$(echo "$STATUS" | jq -r '.initialized // false')
SEALED=$(echo "$STATUS" | jq -r '.sealed // true')

INIT_FILE="/vault/keys/init.json"
ADDRESS_FILE="/vault/keys/address"
PRIVATE_KEY_FILE="/vault/keys/private.key"

if [ "$INITIALIZED" != "true" ]; then
echo "[init] Initializing OpenBao..."

bao operator init -format=json > "$INIT_FILE"
else
echo "[init] Already initialized"

if [ ! -f "$INIT_FILE" ]; then
echo "[init] WARNING: init.json missing, cannot unseal automatically"
echo "[init] Please provide unseal keys manually or persist init.json"
exit 1
fi
fi

# ----------------------------
# UNSEAL
# ----------------------------

UNSEAL_KEY=$(jq -r '.unseal_keys_b64[0]' "$INIT_FILE")

if [ -z "$UNSEAL_KEY" ] || [ "$UNSEAL_KEY" = "null" ]; then
echo "[init] ERROR: missing unseal key"
exit 1
fi

echo "[init] Unsealing OpenBao..."

bao operator unseal "$UNSEAL_KEY" >/dev/null

# wait until unsealed
until [ "$(bao status -format=json | jq -r '.sealed')" = "false" ]; do
echo "[init] waiting for unseal..."
sleep 2
done

echo "[init] OpenBao is unsealed"

echo "[init] Registering ethsign plugin..."

PLUGIN_PATH="/opt/openbao/plugins/ethsign"

if [ ! -f "$PLUGIN_PATH" ]; then
echo "[init] ERROR: plugin binary missing"
exit 1
fi

SHA256=$(sha256sum "$PLUGIN_PATH" | awk '{print $1}')

echo "[init] Plugin SHA256: $SHA256"

bao plugin register -sha256="$SHA256" -command=ethsign secret ethsign || true

echo "[init] Plugin registered (or already exists)"

# ----------------------------
# ETH ACCOUNT INIT
# ----------------------------

if [ -f "$ADDRESS_FILE" ]; then
echo "[init] Account already exists: $(cat $ADDRESS_FILE)"
exit 0
fi

echo "[init] Enabling ethereum secrets engine..."

bao secrets enable -path=ethereum -plugin-name=ethsign plugin || true

if [ ! -f "$PRIVATE_KEY_FILE" ]; then
echo "[init] ERROR: missing private key file"
exit 1
fi

PRIVATE_KEY=$(cat "$PRIVATE_KEY_FILE" | tr -d '\n' | sed 's/^0x//')

echo "[init] Creating ethereum account..."

CREATE_RESPONSE=$(bao write -format=json ethereum/accounts privateKey="${PRIVATE_KEY}")

echo "[init] Raw response:"
echo "$CREATE_RESPONSE"

ADDRESS=$(echo "$CREATE_RESPONSE" | jq -r '.data.address // empty')

if [ -z "$ADDRESS" ]; then
echo "[init] ERROR: failed to extract address"
exit 1
fi

echo "$ADDRESS" > "$ADDRESS_FILE"

echo "[init] Address stored: $ADDRESS"

# ----------------------------
# CLEANUP
# ----------------------------

echo "[init] Removing private key file for security..."
rm -f "$PRIVATE_KEY_FILE"

echo "[init] OpenBao initialization complete"
43 changes: 43 additions & 0 deletions src/vault-demo/local-wallet-signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ethers } from 'ethers'

export class LocalWalletSigner extends ethers.AbstractSigner {
private readonly wallet: ethers.Wallet

constructor(privateKey: string, provider: ethers.JsonRpcProvider) {
super(provider)
this.wallet = new ethers.Wallet(privateKey, provider)
}

async getAddress(): Promise<string> {
return this.wallet.address
}

async signMessage(message: string | Uint8Array): Promise<string> {
return this.wallet.signMessage(message)
}

async signTransaction(tx: ethers.TransactionRequest): Promise<string> {
return this.wallet.signTransaction(tx)
}

async sendTransaction(
tx: ethers.TransactionRequest,
): Promise<ethers.TransactionResponse> {
return this.wallet.sendTransaction(tx)
}

async signTypedData(
domain: ethers.TypedDataDomain,
types: Record<string, ethers.TypedDataField[]>,
value: Record<string, unknown>,
): Promise<string> {
return this.wallet.signTypedData(domain, types, value)
}

connect(provider: ethers.Provider): LocalWalletSigner {
return new LocalWalletSigner(
this.wallet.privateKey,
provider as ethers.JsonRpcProvider,
)
}
}
13 changes: 13 additions & 0 deletions src/vault-demo/openbao.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
disable_mlock = true

listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}

storage "inmem" {}

plugin_directory = "/opt/openbao/plugins"

api_addr = "http://127.0.0.1:8200"
cluster_addr = "http://127.0.0.1:8201"
21 changes: 21 additions & 0 deletions src/vault-demo/signer-backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ethers } from 'ethers'

export type SignerType = 'local' | 'vault'

export interface SignerConfig {
signerType: SignerType
provider: ethers.JsonRpcProvider
chainId: number
// local signer
privateKey?: string
// vault signer
vaultUrl?: string
vaultToken?: string
vaultMount?: string
vaultAccount?: string
}

/**
* All signers extend ethers.AbstractSigner — the type ocean.js requires.
*/
export type SignerBackend = ethers.AbstractSigner
Loading