diff --git a/payment/sudt.go b/payment/sudt.go new file mode 100644 index 00000000..410c731e --- /dev/null +++ b/payment/sudt.go @@ -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) +} diff --git a/transaction/builder/sudt_transfer_unsigned_tx_builder.go b/transaction/builder/sudt_transfer_unsigned_tx_builder.go new file mode 100644 index 00000000..5da32341 --- /dev/null +++ b/transaction/builder/sudt_transfer_unsigned_tx_builder.go @@ -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 + } +}