mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
285 lines
8.4 KiB
Go
285 lines
8.4 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const TONChainID uint64 = 1414483791 // "TONO" in Teleporter namespace
|
|
|
|
// TONPlugin polls a TON HTTP API (toncenter/liteserver) for deposit external messages
|
|
// from the TeleportVault contract and converts them to canonical DepositEvent format.
|
|
type TONPlugin struct {
|
|
apiURL string // TON HTTP API base URL (e.g., https://toncenter.com/api/v2)
|
|
contract string // TeleportVault contract address (base64url or raw)
|
|
client *http.Client
|
|
}
|
|
|
|
func NewTONPlugin(apiURL, contract string) *TONPlugin {
|
|
return &TONPlugin{
|
|
apiURL: apiURL,
|
|
contract: contract,
|
|
client: &http.Client{Timeout: 15 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (t *TONPlugin) Name() string { return "ton" }
|
|
func (t *TONPlugin) ChainID() uint64 { return TONChainID }
|
|
|
|
// PollDeposits fetches transactions to the bridge contract since the given logical time,
|
|
// filters for deposit external out messages, and converts to DepositEvent.
|
|
// TON uses logical time (lt) instead of block height for ordering.
|
|
func (t *TONPlugin) PollDeposits(ctx context.Context, fromLT uint64) ([]DepositEvent, uint64, error) {
|
|
txs, err := t.getTransactions(ctx, fromLT)
|
|
if err != nil {
|
|
return nil, fromLT, fmt.Errorf("ton: get transactions: %w", err)
|
|
}
|
|
|
|
var events []DepositEvent
|
|
var maxLT uint64 = fromLT
|
|
|
|
for _, tx := range txs {
|
|
if tx.LT <= fromLT {
|
|
continue
|
|
}
|
|
if tx.LT > maxLT {
|
|
maxLT = tx.LT
|
|
}
|
|
|
|
// Parse deposit events from external out messages.
|
|
for _, msg := range tx.OutMsgs {
|
|
if msg.MsgType != "ext_out_msg" {
|
|
continue
|
|
}
|
|
deposit, ok := parseDepositFromBOC(msg.Body, tx.LT, tx.Hash)
|
|
if ok {
|
|
events = append(events, deposit)
|
|
}
|
|
}
|
|
}
|
|
|
|
return events, maxLT, nil
|
|
}
|
|
|
|
// QueryBacking calls the get_total_locked get-method on the TeleportVault contract.
|
|
func (t *TONPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
|
result, err := t.runGetMethod(ctx, "get_total_locked")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ton: query backing: %w", err)
|
|
}
|
|
|
|
if len(result.Stack) == 0 {
|
|
return nil, fmt.Errorf("ton: get_total_locked returned empty stack")
|
|
}
|
|
|
|
// TVM returns integers as ["num", "0x..."] tuples.
|
|
val, ok := new(big.Int).SetString(result.Stack[0].Value, 0)
|
|
if !ok {
|
|
return nil, fmt.Errorf("ton: parse total_locked: %s", result.Stack[0].Value)
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
// TON HTTP API types and helpers
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
type tonTransaction struct {
|
|
LT uint64 `json:"lt"`
|
|
Hash string `json:"hash"`
|
|
OutMsgs []tonMessage `json:"out_msgs"`
|
|
}
|
|
|
|
type tonMessage struct {
|
|
MsgType string `json:"msg_type"`
|
|
Body string `json:"body"` // base64-encoded BOC
|
|
}
|
|
|
|
type tonGetMethodResult struct {
|
|
Stack []tonStackEntry `json:"stack"`
|
|
}
|
|
|
|
type tonStackEntry struct {
|
|
Type string `json:"type"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
func (t *TONPlugin) getTransactions(ctx context.Context, afterLT uint64) ([]tonTransaction, error) {
|
|
url := fmt.Sprintf("%s/getTransactions?address=%s&limit=100<=%d&archival=true",
|
|
t.apiURL, t.contract, afterLT)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := t.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var apiResp struct {
|
|
OK bool `json:"ok"`
|
|
Result []tonTransaction `json:"result"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(body, &apiResp); err != nil {
|
|
return nil, fmt.Errorf("parse response: %w", err)
|
|
}
|
|
if !apiResp.OK {
|
|
return nil, fmt.Errorf("api error: %s", apiResp.Error)
|
|
}
|
|
return apiResp.Result, nil
|
|
}
|
|
|
|
func (t *TONPlugin) runGetMethod(ctx context.Context, method string) (*tonGetMethodResult, error) {
|
|
url := fmt.Sprintf("%s/runGetMethod?address=%s&method=%s",
|
|
t.apiURL, t.contract, method)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := t.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var apiResp struct {
|
|
OK bool `json:"ok"`
|
|
Result tonGetMethodResult `json:"result"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(body, &apiResp); err != nil {
|
|
return nil, fmt.Errorf("parse response: %w", err)
|
|
}
|
|
if !apiResp.OK {
|
|
return nil, fmt.Errorf("api error: %s", apiResp.Error)
|
|
}
|
|
return &apiResp.Result, nil
|
|
}
|
|
|
|
// parseDepositFromBOC extracts a DepositEvent from a TON external out message body.
|
|
// The TeleportVault emits deposit events as external messages with the layout:
|
|
//
|
|
// op(32) chain_id(64) nonce(64) lux_recipient(256) amount(coins) timestamp(32)
|
|
//
|
|
// The body is base64-encoded BOC (Bag of Cells).
|
|
func parseDepositFromBOC(bodyB64 string, lt uint64, txHash string) (DepositEvent, bool) {
|
|
data, err := base64.StdEncoding.DecodeString(bodyB64)
|
|
if err != nil {
|
|
return DepositEvent{}, false
|
|
}
|
|
|
|
// Minimal BOC parsing: skip BOC envelope to get to the cell data.
|
|
// TON BOC format: magic(4) + flags(1) + cells(1) + roots(1) + absent(1) + size(1) + ...
|
|
// For single-cell BOCs from external out messages, the cell data starts after the header.
|
|
// The exact offset depends on the BOC serialization variant.
|
|
//
|
|
// We look for the deposit op tag (0x44455031) in the data.
|
|
const depositOp uint32 = 0x44455031
|
|
|
|
cellData := findCellData(data)
|
|
if cellData == nil || len(cellData) < 53 { // minimum: op(4)+chainID(8)+nonce(8)+recipient(32)+varUint(1)
|
|
return DepositEvent{}, false
|
|
}
|
|
|
|
// Verify op tag
|
|
op := be32(cellData[0:4])
|
|
if op != depositOp {
|
|
return DepositEvent{}, false
|
|
}
|
|
|
|
chainID := be64(cellData[4:12])
|
|
nonce := be64(cellData[12:20])
|
|
|
|
// lux_recipient is 256 bits (32 bytes), EVM address in last 20 bytes
|
|
var recipient [20]byte
|
|
copy(recipient[:], cellData[32:52]) // bytes 12-31 of the 32-byte field at offset 20
|
|
|
|
// TON coins use VarUInteger16 encoding: high 4 bits of the byte at offset = byte count N,
|
|
// then N bytes big-endian amount in nanotons.
|
|
amount, _ := decodeVarUInteger16(cellData, 52)
|
|
|
|
_ = chainID // already known (TONChainID)
|
|
|
|
return DepositEvent{
|
|
SrcChainID: TONChainID,
|
|
Nonce: nonce,
|
|
Recipient: recipient,
|
|
Amount: amount,
|
|
TxID: txHash,
|
|
BlockHeight: lt,
|
|
}, true
|
|
}
|
|
|
|
// findCellData locates the cell payload within a BOC envelope.
|
|
// Handles the common single-cell BOC format.
|
|
func findCellData(boc []byte) []byte {
|
|
if len(boc) < 10 {
|
|
return nil
|
|
}
|
|
// Standard BOC magic: b5ee9c72
|
|
if boc[0] == 0xb5 && boc[1] == 0xee && boc[2] == 0x9c && boc[3] == 0x72 {
|
|
// flags(1) size(1) cells(1) roots(1) absent(1) totCellsSize(size bytes) ...
|
|
flags := boc[4]
|
|
sizeBytes := int(flags & 0x07)
|
|
if len(boc) < 5+sizeBytes*3+2 {
|
|
return nil
|
|
}
|
|
// Skip header to cell data. For single-cell BOCs this is straightforward.
|
|
headerSize := 4 + 1 + 1 + 1 + 1 + sizeBytes + sizeBytes
|
|
if headerSize < len(boc) {
|
|
return boc[headerSize:]
|
|
}
|
|
}
|
|
// Fallback: return raw data (for non-standard encoding)
|
|
return boc
|
|
}
|
|
|
|
// decodeVarUInteger16 decodes a TON VarUInteger16 value at the given offset.
|
|
// Encoding: high 4 bits of data[offset] = byte count N, next N bytes = big-endian integer.
|
|
// Returns the decoded integer and the new offset past the consumed bytes.
|
|
func decodeVarUInteger16(data []byte, offset int) (*big.Int, int) {
|
|
if offset >= len(data) {
|
|
return big.NewInt(0), offset
|
|
}
|
|
n := int(data[offset] >> 4) // high nibble = byte count
|
|
offset++
|
|
if n == 0 || offset+n > len(data) {
|
|
return big.NewInt(0), offset
|
|
}
|
|
return new(big.Int).SetBytes(data[offset : offset+n]), offset + n
|
|
}
|
|
|
|
func be32(b []byte) uint32 {
|
|
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
|
}
|
|
|
|
func be64(b []byte) uint64 {
|
|
return uint64(b[0])<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |
|
|
uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7])
|
|
}
|