mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
204 lines
5.6 KiB
Go
204 lines
5.6 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/big"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const ICPChainID uint64 = 0x49435000 // "ICP\x00" in Teleporter namespace
|
|
|
|
// ICPPlugin polls the ICP HTTP gateway for deposit events from the bridge
|
|
// canister. Uses the JSON-based query interface to avoid CBOR dependencies.
|
|
type ICPPlugin struct {
|
|
apiURL string
|
|
canisterID string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewICPPlugin(apiURL, canisterID string) *ICPPlugin {
|
|
return &ICPPlugin{
|
|
apiURL: apiURL,
|
|
canisterID: canisterID,
|
|
client: &http.Client{Timeout: 15 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (i *ICPPlugin) Name() string { return "icp" }
|
|
func (i *ICPPlugin) ChainID() uint64 { return ICPChainID }
|
|
|
|
// PollDeposits queries the bridge canister for deposit events after fromBlock
|
|
// via the ICP HTTP gateway's JSON query interface.
|
|
func (i *ICPPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
|
height, err := i.getBlockHeight(ctx)
|
|
if err != nil {
|
|
return nil, fromBlock, fmt.Errorf("icp: get block height: %w", err)
|
|
}
|
|
if height <= fromBlock {
|
|
return nil, fromBlock, nil
|
|
}
|
|
|
|
deposits, err := i.queryDeposits(ctx, fromBlock)
|
|
if err != nil {
|
|
return nil, fromBlock, fmt.Errorf("icp: query deposits: %w", err)
|
|
}
|
|
|
|
var events []DepositEvent
|
|
for _, d := range deposits {
|
|
recipBytes, err := hexToBytes(d.Recipient)
|
|
if err != nil || len(recipBytes) != 20 {
|
|
log.Printf("WARN: icp: skip deposit nonce %d: invalid recipient %q", d.Nonce, d.Recipient)
|
|
continue
|
|
}
|
|
var recipient [20]byte
|
|
copy(recipient[:], recipBytes)
|
|
|
|
amount, ok := new(big.Int).SetString(d.Amount, 10)
|
|
if !ok {
|
|
log.Printf("WARN: icp: skip deposit nonce %d: invalid amount %q", d.Nonce, d.Amount)
|
|
continue
|
|
}
|
|
|
|
events = append(events, DepositEvent{
|
|
SrcChainID: ICPChainID,
|
|
Nonce: d.Nonce,
|
|
Recipient: recipient,
|
|
Amount: amount,
|
|
TxID: d.TxID,
|
|
BlockHeight: d.BlockHeight,
|
|
})
|
|
}
|
|
|
|
return events, height, nil
|
|
}
|
|
|
|
// QueryBacking calls the get_total_locked query method on the bridge canister.
|
|
func (i *ICPPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
|
var result struct {
|
|
Value string `json:"value"`
|
|
}
|
|
if err := i.canisterQuery(ctx, "get_total_locked", nil, &result); err != nil {
|
|
return nil, fmt.Errorf("icp: query backing: %w", err)
|
|
}
|
|
|
|
val, ok := new(big.Int).SetString(result.Value, 10)
|
|
if !ok {
|
|
return nil, fmt.Errorf("icp: parse total_locked: %s", result.Value)
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────────────
|
|
// ICP HTTP gateway types and helpers
|
|
// ────────────────────────────────────────────────────────────────────
|
|
|
|
type icpDeposit struct {
|
|
Nonce uint64 `json:"nonce"`
|
|
Recipient string `json:"recipient"` // hex EVM address
|
|
Amount string `json:"amount"` // decimal string
|
|
TxID string `json:"tx_id"`
|
|
BlockHeight uint64 `json:"block_height"`
|
|
}
|
|
|
|
func (i *ICPPlugin) getBlockHeight(ctx context.Context) (uint64, error) {
|
|
var result struct {
|
|
Height uint64 `json:"height"`
|
|
}
|
|
if err := i.canisterQuery(ctx, "get_block_height", nil, &result); err != nil {
|
|
return 0, err
|
|
}
|
|
return result.Height, nil
|
|
}
|
|
|
|
func (i *ICPPlugin) queryDeposits(ctx context.Context, fromBlock uint64) ([]icpDeposit, error) {
|
|
args := map[string]interface{}{
|
|
"from_block": fromBlock,
|
|
}
|
|
var result struct {
|
|
Deposits []icpDeposit `json:"deposits"`
|
|
}
|
|
if err := i.canisterQuery(ctx, "get_deposits_after", args, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result.Deposits, nil
|
|
}
|
|
|
|
// canisterQuery sends a JSON query request to the ICP HTTP gateway.
|
|
// The gateway translates JSON to the canister's Candid interface.
|
|
// Request format:
|
|
//
|
|
// POST /api/v2/canister/{canister_id}/query
|
|
// {
|
|
// "method_name": "<method>",
|
|
// "arg": { ... }
|
|
// }
|
|
//
|
|
// Response format:
|
|
//
|
|
// {
|
|
// "status": "replied",
|
|
// "reply": { ... }
|
|
// }
|
|
func (i *ICPPlugin) canisterQuery(ctx context.Context, method string, args interface{}, dest interface{}) error {
|
|
reqBody := map[string]interface{}{
|
|
"method_name": method,
|
|
}
|
|
if args != nil {
|
|
reqBody["arg"] = args
|
|
}
|
|
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/api/v2/canister/%s/query", i.apiURL, i.canisterID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := i.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("icp query %s: %d: %s", method, resp.StatusCode, respBody)
|
|
}
|
|
|
|
var envelope struct {
|
|
Status string `json:"status"`
|
|
Reply json.RawMessage `json:"reply"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &envelope); err != nil {
|
|
return fmt.Errorf("icp: parse response: %w", err)
|
|
}
|
|
if envelope.Status == "rejected" {
|
|
return fmt.Errorf("icp: canister rejected: %s", envelope.Error)
|
|
}
|
|
if envelope.Status != "replied" {
|
|
return fmt.Errorf("icp: unexpected status: %s", envelope.Status)
|
|
}
|
|
if dest != nil {
|
|
return json.Unmarshal(envelope.Reply, dest)
|
|
}
|
|
return nil
|
|
}
|