Skip to content

Commit

Permalink
feat: add sudt payment
Browse files Browse the repository at this point in the history
  • Loading branch information
shaojunda committed Dec 29, 2020
1 parent 70ee927 commit 4938ff8
Show file tree
Hide file tree
Showing 2 changed files with 361 additions and 0 deletions.
113 changes: 113 additions & 0 deletions payment/sudt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package payment

import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/nervosnetwork/ckb-sdk-go/address"
"github.com/nervosnetwork/ckb-sdk-go/collector"
"github.com/nervosnetwork/ckb-sdk-go/crypto"
"github.com/nervosnetwork/ckb-sdk-go/indexer"
"github.com/nervosnetwork/ckb-sdk-go/rpc"
"github.com/nervosnetwork/ckb-sdk-go/transaction"
"github.com/nervosnetwork/ckb-sdk-go/transaction/builder"
"github.com/nervosnetwork/ckb-sdk-go/types"
"github.com/nervosnetwork/ckb-sdk-go/utils"
"github.com/pkg/errors"
"math/big"
)

type Sudt struct {
Sender *types.Script
Receiver *types.Script
UUID string
Amount *big.Int
FeeRate uint64

tx *types.Transaction
systemScripts *utils.SystemScripts
}

// NewSudt returns a new NewSudt object
func NewSudt(senderAddr, receiverAddr, uuid, amount string, feeRate uint64, systemScripts *utils.SystemScripts) (*Sudt, error) {
parsedSenderAddr, err := address.Parse(senderAddr)
if err != nil {
return nil, err
}
parsedReceiverAddr, err := address.Parse(receiverAddr)
if err != nil {
return nil, err
}
n, b := big.NewInt(0).SetString(amount, 10)
if !b {
return nil, errors.WithMessage(err, "invalid amount")
}

return &Sudt{
Sender: parsedSenderAddr.Script,
Receiver: parsedReceiverAddr.Script,
UUID: uuid,
Amount: n,
FeeRate: feeRate,
systemScripts: systemScripts,
}, nil
}

// GenerateTransferSudtUnsignedTx generate an unsigned transaction for transfer sudt
func (s *Sudt) GenerateTransferSudtUnsignedTx(client rpc.Client) (*types.Transaction, error) {
// udt type script
udtType := &types.Script{
CodeHash: s.systemScripts.SUDTCell.CellHash,
HashType: s.systemScripts.SUDTCell.HashType,
Args: common.FromHex(s.UUID),
}
searchKey := &indexer.SearchKey{
Script: s.Sender,
ScriptType: indexer.ScriptTypeLock,
}
// sudt Iterator
sudtCollector := collector.NewLiveCellCollector(client, searchKey, indexer.SearchOrderAsc, indexer.SearchLimit, "")
sudtCollector.TypeScript = udtType
sudtIterator, err := sudtCollector.Iterator()
if err != nil {
return nil, errors.Errorf("collect sudt cells error: %v", err)
}
// ckb Iterator
ckbCollector := collector.NewLiveCellCollector(client, searchKey, indexer.SearchOrderAsc, indexer.SearchLimit, "")
ckbCollector.EmptyData = true
ckbIterator, err := ckbCollector.Iterator()
if err != nil {
return nil, errors.Errorf("collect sudt cells error: %v", err)
}
director := builder.Director{}
txBuilder := &builder.SudtTransferUnsignedTxBuilder{
Sender: s.Sender,
Receiver: s.Receiver,
FeeRate: s.FeeRate,
CkbIterator: ckbIterator,
SUDTIterator: sudtIterator,
SystemScripts: s.systemScripts,
TransferAmount: s.Amount,
UUID: s.UUID,
}

director.SetBuilder(txBuilder)
tx, _, err := director.Generate()
s.tx = tx

return tx, err
}

// SignTx sign an unsigned sudt transfer transaction and return an signed transaction
func (s *Sudt) SignTx(key crypto.Key) (*types.Transaction, error) {
err := transaction.SingleSegmentSignTransaction(s.tx, 0, len(s.tx.Witnesses), transaction.EmptyWitnessArg, key)
if err != nil {
return nil, fmt.Errorf("sign transaction error: %v", err)
}
return s.tx, nil
}

// Send can send a tx to tx pool
func (s *Sudt) Send(client rpc.Client) (*types.Hash, error) {
return client.SendTransaction(context.Background(), s.tx)
}
248 changes: 248 additions & 0 deletions transaction/builder/sudt_transfer_unsigned_tx_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package builder

import (
"bytes"
"github.com/ethereum/go-ethereum/common"
"github.com/nervosnetwork/ckb-sdk-go/collector"
"github.com/nervosnetwork/ckb-sdk-go/transaction"
"github.com/nervosnetwork/ckb-sdk-go/types"
"github.com/nervosnetwork/ckb-sdk-go/utils"
"github.com/pkg/errors"
"math"
"math/big"
)

var _ UnsignedTxBuilder = (*SudtTransferUnsignedTxBuilder)(nil)

type SudtTransferUnsignedTxBuilder struct {
Sender *types.Script
Receiver *types.Script
FeeRate uint64
CkbIterator collector.CellCollectionIterator
SUDTIterator collector.CellCollectionIterator
SystemScripts *utils.SystemScripts
TransferAmount *big.Int
UUID string

tx *types.Transaction
result *collector.LiveCellCollectResult
ckbChangeOutputIndex *collector.ChangeOutputIndex
sUDTChangeOutputIndex *collector.ChangeOutputIndex
}

func (s *SudtTransferUnsignedTxBuilder) NewTransaction() {
s.tx = &types.Transaction{}
}

func (s *SudtTransferUnsignedTxBuilder) BuildVersion() {
s.tx.Version = 0
}

func (s *SudtTransferUnsignedTxBuilder) BuildHeaderDeps() {
s.tx.HeaderDeps = []types.Hash{}
}

func (s *SudtTransferUnsignedTxBuilder) BuildCellDeps() {
s.tx.CellDeps = []*types.CellDep{
{
OutPoint: s.SystemScripts.SecpSingleSigCell.OutPoint,
DepType: types.DepTypeDepGroup,
},
{
OutPoint: s.SystemScripts.SUDTCell.OutPoint,
DepType: s.SystemScripts.SUDTCell.DepType,
},
}
}

func (s *SudtTransferUnsignedTxBuilder) BuildOutputsAndOutputsData() error {
udtType := &types.Script{
CodeHash: s.SystemScripts.SUDTCell.CellHash,
HashType: s.SystemScripts.SUDTCell.HashType,
Args: common.FromHex(s.UUID),
}
// set receiver sudt output
s.tx.Outputs = append(s.tx.Outputs, &types.CellOutput{
Capacity: udtCellCapacity,
Lock: &types.Script{
CodeHash: s.Receiver.CodeHash,
HashType: s.Receiver.HashType,
Args: s.Receiver.Args,
},
Type: udtType,
})
s.tx.OutputsData = append(s.tx.OutputsData, utils.GenerateSudtAmount(s.TransferAmount))

// set ckb change output
s.tx.Outputs = append(s.tx.Outputs, &types.CellOutput{
Capacity: 0,
Lock: s.Sender,
})
s.tx.OutputsData = append(s.tx.OutputsData, []byte{})
// set ckb change output index
s.ckbChangeOutputIndex = &collector.ChangeOutputIndex{Value: 1}

// set sudt change output
s.tx.Outputs = append(s.tx.Outputs, &types.CellOutput{
Capacity: udtCellCapacity,
Lock: s.Sender,
Type: udtType,
})
s.tx.OutputsData = append(s.tx.OutputsData, sudtDataPlaceHolder)
// set sudt change output index
s.sUDTChangeOutputIndex = &collector.ChangeOutputIndex{Value: 2}

return nil
}

func (s *SudtTransferUnsignedTxBuilder) BuildInputsAndWitnesses() error {
if s.TransferAmount == nil {
return errors.New("transfer amount is required")
}
// collect sudt cells first
err := s.collectSUDTCells()
if err != nil {
return err
}

// then collect ckb cells
err = s.collectCkbCells()
if err != nil {
return err
}
return nil
}

func (s *SudtTransferUnsignedTxBuilder) UpdateChangeOutput() error {
// update sudt change output first
totalAmount := s.result.Options["totalAmount"].(*big.Int)
if totalAmount.Cmp(s.TransferAmount) > 0 && bytes.Compare(s.tx.OutputsData[s.sUDTChangeOutputIndex.Value], sudtDataPlaceHolder) == 0 {
s.tx.OutputsData[s.sUDTChangeOutputIndex.Value] = utils.GenerateSudtAmount(big.NewInt(0).Sub(totalAmount, s.TransferAmount))
}
if totalAmount.Cmp(s.TransferAmount) == 0 {
s.tx.Outputs = utils.RemoveCellOutput(s.tx.Outputs, s.sUDTChangeOutputIndex.Value)
s.tx.OutputsData = utils.RemoveCellOutputData(s.tx.OutputsData, s.sUDTChangeOutputIndex.Value)
}

// then update ckb change output
fee, err := transaction.CalculateTransactionFee(s.tx, s.FeeRate)
if err != nil {
return err
}
changeCapacity := s.result.Capacity - s.tx.OutputsCapacity() - fee
s.tx.Outputs[s.ckbChangeOutputIndex.Value].Capacity = changeCapacity

return nil
}

func (s *SudtTransferUnsignedTxBuilder) GetResult() (*types.Transaction, [][]int) {
return s.tx, nil
}

func (s *SudtTransferUnsignedTxBuilder) collectCkbCells() error {
for s.CkbIterator.HasNext() {
liveCell, err := s.CkbIterator.CurrentItem()
if err != nil {
return err
}
s.result.Capacity += liveCell.Output.Capacity
s.result.LiveCells = append(s.result.LiveCells, liveCell)
input := &types.CellInput{
Since: 0,
PreviousOutput: &types.OutPoint{
TxHash: liveCell.OutPoint.TxHash,
Index: liveCell.OutPoint.Index,
},
}
s.tx.Inputs = append(s.tx.Inputs, input)
s.tx.Witnesses = append(s.tx.Witnesses, []byte{})
ok, err := s.isCkbEnough()
if err != nil {
return err
}
if ok {
return nil
}
err = s.CkbIterator.Next()
if err != nil {
return err
}
}
return errors.New("insufficient ckb balance")
}

func (s *SudtTransferUnsignedTxBuilder) collectSUDTCells() error {
s.result = &collector.LiveCellCollectResult{}
for s.SUDTIterator.HasNext() {
liveCell, err := s.SUDTIterator.CurrentItem()
if err != nil {
return err
}
s.result.Capacity += liveCell.Output.Capacity
s.result.LiveCells = append(s.result.LiveCells, liveCell)
// init totalAmount
if _, ok := s.result.Options["totalAmount"]; !ok {
s.result.Options = make(map[string]interface{})
s.result.Options["totalAmount"] = big.NewInt(0)
}
amount, err := utils.ParseSudtAmount(liveCell.OutputData)
if err != nil {
return errors.WithMessage(err, "sudt amount parse error")
}
totalAmount := s.result.Options["totalAmount"].(*big.Int)
s.result.Options["totalAmount"] = big.NewInt(0).Add(totalAmount, amount)
input := &types.CellInput{
Since: 0,
PreviousOutput: &types.OutPoint{
TxHash: liveCell.OutPoint.TxHash,
Index: liveCell.OutPoint.Index,
},
}
s.tx.Inputs = append(s.tx.Inputs, input)
s.tx.Witnesses = append(s.tx.Witnesses, []byte{})
if len(s.tx.Witnesses[0]) == 0 {
s.tx.Witnesses[0] = transaction.EmptyWitnessArgPlaceholder
}
// stop collect
if s.isSUDTEnough() {
return nil
}
err = s.SUDTIterator.Next()
if err != nil {
return err
}
}
return errors.New("insufficient sudt balance")
}

func (s *SudtTransferUnsignedTxBuilder) isSUDTEnough() bool {
totalAmount := s.result.Options["totalAmount"].(*big.Int)
if totalAmount.Cmp(s.TransferAmount) >= 0 {
return true
}
return false
}

func (s *SudtTransferUnsignedTxBuilder) isCkbEnough() (bool, error) {
inputsCapacity := big.NewInt(0).SetUint64(s.result.Capacity)
outputsCapacity := big.NewInt(0).SetUint64(s.tx.OutputsCapacity())
changeCapacity := big.NewInt(0).Sub(inputsCapacity, outputsCapacity)
if changeCapacity.Cmp(big.NewInt(0)) > 0 {
fee, err := transaction.CalculateTransactionFee(s.tx, s.FeeRate)
if err != nil {
return false, err
}
changeCapacity = big.NewInt(0).Sub(changeCapacity, big.NewInt(0).SetUint64(fee))
changeOutput := s.tx.Outputs[s.ckbChangeOutputIndex.Value]
changeOutputData := s.tx.OutputsData[s.ckbChangeOutputIndex.Value]

changeOutputCapacity := big.NewInt(0).SetUint64(changeOutput.OccupiedCapacity(changeOutputData) * uint64(math.Pow10(8)))
if changeCapacity.Cmp(changeOutputCapacity) >= 0 {
return true, nil
} else {
return false, nil
}
} else {
return false, nil
}
}

0 comments on commit 4938ff8

Please sign in to comment.