Skip to main content
Brane supports external signing via the Signer interface. This allows you to use keys managed by third-party services (Privy, Fireblocks, AWS KMS) or hardware wallets without exposing the private key to your application.

The Interface

To implement a custom signer, you must implement the Signer interface.
public interface Signer {
    Signature signTransaction(UnsignedTransaction tx, long chainId);
    Signature signMessage(byte[] message);
    Address address();
}

Example: External Service Integration

Here is how you might implement a signer that delegates to an external API (e.g., Privy or a backend KMS).
import io.brane.core.crypto.Signer;
import io.brane.core.crypto.Signature;
import io.brane.core.types.Address;
import io.brane.core.tx.UnsignedTransaction;
import io.brane.core.crypto.Keccak256;

// Dummy interface for the example
interface ExternalService {
    Signature sign(String keyId, byte[] hash);
}

public class RemoteSigner implements Signer {
    private final String keyId;
    private final Address address;
    private final ExternalService service;

    public RemoteSigner(String keyId, Address address, ExternalService service) {
        this.keyId = keyId;
        this.address = address;
        this.service = service;
    }

    @Override
    public Address address() {
        return address;
    }

    @Override
    public Signature signTransaction(UnsignedTransaction tx, long chainId) {
        // 1. Get the transaction hash (preimage)
        byte[] preimage = tx.encodeForSigning(chainId);
        byte[] hash = Keccak256.hash(preimage);

        // 2. Request signature from external service
        // (This is where you call Privy, AWS KMS, etc.)
        return service.sign(this.keyId, hash);
    }

    @Override
    public Signature signMessage(byte[] message) {
        // 1. Create the EIP-191 prefixed message
        byte[] prefix = ("\u0019Ethereum Signed Message:\n" + message.length)
                .getBytes(java.nio.charset.StandardCharsets.UTF_8);
        byte[] prefixedMessage = java.nio.ByteBuffer.allocate(prefix.length + message.length)
                .put(prefix)
                .put(message)
                .array();

        // 2. Hash the prefixed message
        byte[] hash = Keccak256.hash(prefixedMessage);
        
        // 3. Request signature for the hash
        Signature sig = service.sign(this.keyId, hash);
        
        // Adjust v to be 27 or 28 for EIP-191 compatibility
        return new Signature(sig.r(), sig.s(), sig.v() + 27);
    }
}

Usage with WalletClient

Once you have your custom signer, simply pass it to the WalletClient builder.
import io.brane.rpc.DefaultWalletClient;
import io.brane.rpc.BraneProvider;
import io.brane.rpc.PublicClient;
import io.brane.core.chain.ChainProfiles;

// Assume these are defined
BraneProvider provider = ...;
PublicClient publicClient = ...;
ExternalService service = ...;

Signer mySigner = new RemoteSigner(
    "key-123", 
    new Address("0x..."), 
    service
);

WalletClient wallet = DefaultWalletClient.create(
    provider,
    publicClient,
    mySigner,
    ChainProfiles.ANVIL_LOCAL
);