Signer Implementation Guide
🎯 Purpose
This guide explains how to implement blockchain signers for the Wallet Service. Currently covers TON and TRON signers.
📐 Signer Interface
Interface Definition
File: services/wallet/internal/wallet/service/signer/interfaces.go
type Curve string
type Chain string
type PrivKey []byte
type PubKey []byte
// Deriver derives keys from mnemonics
type Deriver interface {
FromMnemonic(m string, path string, curve Curve) (PrivKey, PubKey, error)
}
// Signer renders an address and signs transactions
type Signer interface {
Address(pub PubKey, chain Chain) (string, error)
SignTx(chain Chain, priv PrivKey, payload []byte) ([]byte, error)
}
Design Principles
- Stateless: Signers don’t store keys (receive them as parameters)
- Pure Functions: Same input → same output
- Chain-Specific: Each chain has its own signer
- Testable: Easy to test with known vectors
🔵 Task 0.1: TON Signer Implementation
Current Status
File: services/wallet/internal/wallet/service/signer/ton_tron_placeholders.go
// CURRENT (BROKEN)
type tonSigner struct{}
func (t *tonSigner) Address(pub PubKey, chain Chain) (string, error) {
return "", errors.New("ton signer not implemented") // ❌
}
func (t *tonSigner) SignTx(chain Chain, priv PrivKey, payload []byte) ([]byte, error) {
return nil, errors.New("ton signer not implemented") // ❌
}
Implementation Steps
Step 1: Import Dependencies
import (
"crypto/ed25519"
"fmt"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"
)
Note: tonutils-go v1.14.1 already in go.mod!
Step 2: Implement TON Signer Struct
type tonSigner struct {
// Stateless - no fields needed
// All data passed as parameters
}
// Constructor (if needed)
func NewTONSigner() *tonSigner {
return &tonSigner{}
}
Step 3: Implement Address() Method
Goal: Generate TON address from public key
func (t *tonSigner) Address(pub PubKey, chain Chain) (string, error) {
// Validate input
if len(pub) != ed25519.PublicKeySize {
return "", fmt.Errorf("invalid public key length: expected %d, got %d",
ed25519.PublicKeySize, len(pub))
}
// Convert to ed25519.PublicKey
publicKey := ed25519.PublicKey(pub)
// Determine workchain (0 for mainnet, -1 for masterchain)
workchain := 0
if chain == Chain("TON_TESTNET") {
// Testnet still uses workchain 0, just different network ID
workchain = 0
}
// Create TON address using tonutils-go
// This matches the logic in ton_wallet_generator_v4r2.go
addr := address.NewAddress(byte(workchain), 0, publicKey)
// Return bounceable form (user-facing)
// EQ for mainnet, UQ for testnet
return addr.Bounce(true).String(), nil
}
Key Points:
- ✅ Uses same logic as
ton_wallet_generator_v4r2.go - ✅ Returns bounceable form (EQ…/UQ…)
- ✅ Validates input
- ✅ Supports testnet vs mainnet
Step 4: Implement SignTx() Method
Goal: Sign TON transaction
func (t *tonSigner) SignTx(chain Chain, priv PrivKey, payload []byte) ([]byte, error) {
// Validate private key
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key length: expected %d, got %d",
ed25519.PrivateKeySize, len(priv))
}
// Convert to ed25519.PrivateKey
privateKey := ed25519.PrivateKey(priv)
// Sign the payload using ed25519
// For TON, payload is typically the message cell hash
signature := ed25519.Sign(privateKey, payload)
return signature, nil
}
Alternative (Full TON Transaction Building):
func (t *tonSigner) SignTx(chain Chain, priv PrivKey, payload []byte) ([]byte, error) {
privateKey := ed25519.PrivateKey(priv)
// If payload is a complete transaction structure:
// 1. Parse payload as TON transaction
// 2. Build message cell
// 3. Sign cell hash
// 4. Return signed transaction bytes
// For now, simple signing is sufficient
signature := ed25519.Sign(privateKey, payload)
return signature, nil
}
Step 5: Write Unit Tests
File: services/wallet/internal/wallet/service/signer/ton_signer_test.go (create)
package signers
import (
"crypto/ed25519"
"testing"
"strings"
"github.com/stretchr/testify/assert"
"github.com/xssnick/tonutils-go/ton/wallet"
)
func TestTONSigner_Address(t *testing.T) {
// Use known mnemonic
mnemonic := []string{
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "abandon",
"abandon", "abandon", "abandon", "art",
}
// Derive private key
privateKey, err := wallet.SeedToPrivateKey(mnemonic, "", true)
assert.NoError(t, err)
// Get public key
publicKey := privateKey.Public().(ed25519.PublicKey)
// Create signer
signer := &tonSigner{}
// Generate address
address, err := signer.Address(PubKey(publicKey), Chain("TON"))
assert.NoError(t, err)
// Verify format
assert.True(t, strings.HasPrefix(address, "EQ") || strings.HasPrefix(address, "UQ"))
assert.Greater(t, len(address), 40)
t.Logf("Generated address: %s", address)
}
func TestTONSigner_SignTx(t *testing.T) {
// Generate keypair
publicKey, privateKey, err := ed25519.GenerateKey(nil)
assert.NoError(t, err)
// Create signer
signer := &tonSigner{}
// Sign test message
message := []byte("test transaction payload")
signature, err := signer.SignTx(Chain("TON"), PrivKey(privateKey), message)
assert.NoError(t, err)
assert.NotNil(t, signature)
assert.Equal(t, ed25519.SignatureSize, len(signature))
// Verify signature
valid := ed25519.Verify(publicKey, message, signature)
assert.True(t, valid, "Signature should be valid")
}
func TestTONSigner_IntegrationWithGenerator(t *testing.T) {
// This test ensures signer works with TON Wallet Generator v4R2
// Generate wallet using v4R2 generator
generator := NewTONWalletGeneratorV4R2("test-encryption-key-32-bytes-long")
wallet, root, err := generator.GenerateWallet(123456789, "Test Wallet", true)
assert.NoError(t, err)
// Decrypt mnemonic
mnemonic, err := generator.DecryptData(root.EncryptedMnemonic)
assert.NoError(t, err)
// Derive private key
words := strings.Fields(mnemonic)
privateKey, err := wallet.SeedToPrivateKey(words, "", true)
assert.NoError(t, err)
// Get public key
publicKey := privateKey.Public().(ed25519.PublicKey)
// Use signer to generate address
signer := &tonSigner{}
signerAddress, err := signer.Address(PubKey(publicKey), Chain("TON"))
assert.NoError(t, err)
// MUST match the address from generator
assert.Equal(t, wallet.Address, signerAddress,
"Signer address must match generator address")
t.Logf("Generator address: %s", wallet.Address)
t.Logf("Signer address: %s", signerAddress)
}
Complete Implementation
File: Replace ton_tron_placeholders.go (TON part only)
package signers
import (
"crypto/ed25519"
"fmt"
"github.com/xssnick/tonutils-go/address"
)
// tonSigner implements TON-specific signing operations
type tonSigner struct{}
// Address generates a TON address from a public key
// Matches the behavior of TONWalletGeneratorV4R2
func (t *tonSigner) Address(pub PubKey, chain Chain) (string, error) {
// Validate public key length
if len(pub) != ed25519.PublicKeySize {
return "", fmt.Errorf("invalid public key length for TON: expected %d, got %d",
ed25519.PublicKeySize, len(pub))
}
// Convert to ed25519.PublicKey
publicKey := ed25519.PublicKey(pub)
// Determine workchain
// Workchain 0 = basechain (user wallets)
// Workchain -1 = masterchain (validators)
workchain := byte(0)
// Create TON address using tonutils-go
// This creates a proper v4R2 wallet contract address
// Same logic as in ton_wallet_generator_v4r2.go line 95
addr := address.NewAddress(workchain, 0, publicKey)
// Return bounceable form (EQ for mainnet, UQ for testnet)
// Bounceable = true means return EQ/UQ prefix
return addr.Bounce(true).String(), nil
}
// SignTx signs a TON transaction with a private key
// For TON, the payload is typically the message cell hash
func (t *tonSigner) SignTx(chain Chain, priv PrivKey, payload []byte) ([]byte, error) {
// Validate private key length
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key length for TON: expected %d, got %d",
ed25519.PrivateKeySize, len(priv))
}
// Convert to ed25519.PrivateKey
privateKey := ed25519.PrivateKey(priv)
// Sign using ed25519
// For TON transactions, this is typically signing the cell hash
signature := ed25519.Sign(privateKey, payload)
// Signature is 64 bytes for ed25519
return signature, nil
}
✅ Success Criteria
Task 0.1 Complete When:
- [x] tonSigner implements Signer interface
- [x] Address() generates valid TON addresses (EQ… format)
- [x] Address() matches TON Wallet Gen v4R2 output
- [x] SignTx() produces valid ed25519 signatures
- [x] All unit tests pass
- [x] Integration test with generator passes
- [x] No “not implemented” errors
- [x] Can sign real withdrawal transactions