diff --git a/go.mod b/go.mod index e4ae4a8..f5496c7 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/ethereum/go-ethereum v1.10.9 // indirect github.com/joho/godotenv v1.3.0 github.com/koinos/go-prompt v0.0.0-20221201222302-dba4c3542a91 - github.com/koinos/koinos-proto-golang/v2 v2.3.0 + github.com/koinos/koinos-proto-golang/v2 v2.6.0 github.com/koinos/koinos-util-golang/v2 v2.0.1 github.com/minio/sio v0.3.0 github.com/multiformats/go-multihash v0.1.0 diff --git a/go.sum b/go.sum index 65991b5..6f61448 100644 --- a/go.sum +++ b/go.sum @@ -846,6 +846,8 @@ github.com/koinos/go-prompt v0.0.0-20221201222302-dba4c3542a91/go.mod h1:Q5ndhnC github.com/koinos/koinos-proto-golang/v2 v2.0.2/go.mod h1:BJg2czLOSVW2/ExpK/SBIrcN+N9bu7ejVyDnNYFuW/o= github.com/koinos/koinos-proto-golang/v2 v2.3.0 h1:Snh50MJuV4jwVgtahB4XuOl21MEkjo+6gJJC/wMNl2k= github.com/koinos/koinos-proto-golang/v2 v2.3.0/go.mod h1:BJg2czLOSVW2/ExpK/SBIrcN+N9bu7ejVyDnNYFuW/o= +github.com/koinos/koinos-proto-golang/v2 v2.6.0 h1:g2m1XKJ3VUl6T8zoeaC5PG5zVcitY8cMgLJUvIisosw= +github.com/koinos/koinos-proto-golang/v2 v2.6.0/go.mod h1:BJg2czLOSVW2/ExpK/SBIrcN+N9bu7ejVyDnNYFuW/o= github.com/koinos/koinos-util-golang/v2 v2.0.1 h1:sKXIpko9BSPJ9Nymb4ES+2rtbQKYDapCv2HjMYuhuzo= github.com/koinos/koinos-util-golang/v2 v2.0.1/go.mod h1:Iw80hOODPUeXU1rnfUgtn2hMT2GiuZ2h+bomNGL1q2A= github.com/koinos/protobuf-go v1.27.2-0.20211026185306-2456c83214fe h1:PJ+2AnN4ibN2WxldiClplZZosQNPnXj7S5vOeFNtV+M= diff --git a/internal/cli/abi.go b/internal/cli/abi.go index 4168def..eafb801 100644 --- a/internal/cli/abi.go +++ b/internal/cli/abi.go @@ -109,11 +109,13 @@ func (abi *ABI) GetFiles() (*protoregistry.Files, error) { // ABIMethod represents an ABI method descriptor type ABIMethod struct { - Argument string `json:"argument"` - Return string `json:"return"` - EntryPoint string `json:"entry-point"` - Description string `json:"description"` - ReadOnly bool `json:"read-only"` + Argument string `json:"argument"` + Return string `json:"return"` + EntryPoint uint64 `json:"entry_point"` + Description string `json:"description"` + ReadOnly bool `json:"read_only"` + EntryPointOld string `json:"entry-point"` + ReadOnlyOld bool `json:"read-only"` } // ContractInfo represents the information about a contract @@ -424,3 +426,15 @@ func ParseResultToMessage(cmd *CommandParseResult, contracts Contracts) (proto.M return DataToMessage(cmd.Args, md) } + +func GetEntryPoint(method *ABIMethod) (uint64, error) { + if len(method.EntryPointOld) > 2 && method.EntryPointOld[:2] == "0x" { + return strconv.ParseUint(method.EntryPointOld[2:], 16, 32) + } else { + return uint64(method.EntryPoint), nil + } +} + +func GetReadOnly(method *ABIMethod) bool { + return method.ReadOnly || method.ReadOnlyOld +} diff --git a/internal/cli/abi_test.go b/internal/cli/abi_test.go index a856f0d..2ce38a5 100644 --- a/internal/cli/abi_test.go +++ b/internal/cli/abi_test.go @@ -14,21 +14,48 @@ var ( "argument": "abi_test.empty_arguments", "return": "abi_test.empty_result", "description": "Empty arguments", - "entry_point": "0x2e1cfa82", + "entry_point": 773651074, + "read_only": false + }, + "simple": { + "argument": "abi_test.simple_arguments", + "return": "abi_test.simple_result", + "description": "Simple arguments", + "entry_point": 2812517234, + "read_only": false + }, + "nested": { + "argument": "abi_test.nested_arguments", + "return": "abi_test.nested_result", + "description": "Nested arguments", + "entry_point": 590701278, + "read_only": false + } + }, + "types": "Cr4ECit0ZXN0X2FiaS9hc3NlbWJseS9wcm90by9jb25zdGVsbGF0aW9uLnByb3RvEghhYmlfdGVzdBoUa29pbm9zL29wdGlvbnMucHJvdG8iEQoPZW1wdHlfYXJndW1lbnRzIg4KDGVtcHR5X3Jlc3VsdCJOChBzaW1wbGVfYXJndW1lbnRzEg4KAmlkGAEgASgNUgJpZBISCgRuYW1lGAIgASgJUgRuYW1lEhYKBmFjdGl2ZRgDIAEoCFIGYWN0aXZlIg8KDXNpbXBsZV9yZXN1bHQiYgoQbmVzdGVkX2FyZ3VtZW50cxISCgRuYW1lGAEgASgJUgRuYW1lEiQKBGRhdGEYAiABKAsyEC5hYmlfdGVzdC5kYXRhX2NSBGRhdGESFAoFdmFsdWUYAyABKA1SBXZhbHVlIg8KDW5lc3RlZF9yZXN1bHQiRAoGZGF0YV9hEhQKBXZhbHVlGAEgASgNUgV2YWx1ZRISCgRuYW1lGAIgASgJUgRuYW1lEhAKA251bRgDIAEoCVIDbnVtIjQKBmRhdGFfYhIWCgZhY3RpdmUYASABKAhSBmFjdGl2ZRISCgRuYW1lGAIgASgJUgRuYW1lInIKBmRhdGFfYxISCgRuYW1lGAEgASgJUgRuYW1lEh4KAWEYAiABKAsyEC5hYmlfdGVzdC5kYXRhX2FSAWESFAoFdmFsdWUYAyABKA1SBXZhbHVlEh4KAWIYBCABKAsyEC5hYmlfdGVzdC5kYXRhX2JSAWJiBnByb3RvMw==" + }` + + JSONABI_OLD = `{ + "methods": { + "empty": { + "argument": "abi_test.empty_arguments", + "return": "abi_test.empty_result", + "description": "Empty arguments", + "entry-point": "0x2e1cfa82", "read-only": false }, "simple": { "argument": "abi_test.simple_arguments", "return": "abi_test.simple_result", "description": "Simple arguments", - "entry_point": "0xa7a39b72", + "entry-point": "0xa7a39b72", "read-only": false }, "nested": { "argument": "abi_test.nested_arguments", "return": "abi_test.nested_result", "description": "Nested arguments", - "entry_point": "0x233562de", + "entry-point": "0x233562de", "read-only": false } }, @@ -36,16 +63,16 @@ var ( }` ) -func loadABI(t *testing.T) *ABI { +func loadABI(t *testing.T, jsonABI string) *ABI { var abi ABI - err := json.Unmarshal([]byte(JSONABI), &abi) + err := json.Unmarshal([]byte(jsonABI), &abi) assert.NoError(t, err) return &abi } -func loadContracts(t *testing.T) Contracts { +func loadContracts(t *testing.T, jsonABI string) Contracts { contracts := Contracts(make(map[string]*ContractInfo)) - abi := loadABI(t) + abi := loadABI(t, jsonABI) files, err := abi.GetFiles() assert.NoError(t, err) @@ -68,7 +95,21 @@ func testMethod(t *testing.T, contracts Contracts, method string, expectedArgume } func TestABI(t *testing.T) { - contracts := loadContracts(t) + contracts := loadContracts(t, JSONABI) + + // Test empty arguments + testMethod(t, contracts, "abi_test.empty", []string{}) + + // Test simple arguments + testMethod(t, contracts, "abi_test.simple", []string{"id", "name", "active"}) + + // Test nested arguments + testMethod(t, contracts, "abi_test.nested", []string{"name", "data.name", "data.a.value", "data.a.name", "data.a.num", + "data.value", "data.b.active", "data.b.name", "value"}) +} + +func TestABIOld(t *testing.T) { + contracts := loadContracts(t, JSONABI_OLD) // Test empty arguments testMethod(t, contracts, "abi_test.empty", []string{}) diff --git a/internal/cli/contract_commands.go b/internal/cli/contract_commands.go index 804e761..4bd5366 100644 --- a/internal/cli/contract_commands.go +++ b/internal/cli/contract_commands.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "strconv" "github.com/btcsuite/btcutil/base58" "github.com/koinos/koinos-cli/internal/cliutil" @@ -125,7 +124,7 @@ func (c *RegisterCommand) Execute(ctx context.Context, ee *ExecutionEnvironment) // Create the command var cmd *CommandDeclaration - if method.ReadOnly { + if GetReadOnly(method) { cmd = NewCommandDeclaration(commandName, method.Description, false, NewReadContractCommand, params...) } else { cmd = NewCommandDeclaration(commandName, method.Description, false, NewWriteContractCommand, params...) @@ -171,7 +170,7 @@ func (c *ReadContractCommand) Execute(ctx context.Context, ee *ExecutionEnvironm contract := ee.Contracts.GetFromMethodName(c.ParseResult.CommandName) - entryPoint, err := strconv.ParseUint(ee.Contracts.GetMethod(c.ParseResult.CommandName).EntryPoint[2:], 16, 32) + entryPoint, err := GetEntryPoint(ee.Contracts.GetMethod(c.ParseResult.CommandName)) if err != nil { return nil, err } @@ -310,7 +309,7 @@ func (c *WriteContractCommand) Execute(ctx context.Context, ee *ExecutionEnviron contract := ee.Contracts.GetFromMethodName(c.ParseResult.CommandName) - entryPoint, err := strconv.ParseUint(ee.Contracts.GetMethod(c.ParseResult.CommandName).EntryPoint[2:], 16, 32) + entryPoint, err := GetEntryPoint(ee.Contracts.GetMethod(c.ParseResult.CommandName)) if err != nil { return nil, err } diff --git a/internal/cli/parser.go b/internal/cli/parser.go index 1e5cea5..4e0d252 100644 --- a/internal/cli/parser.go +++ b/internal/cli/parser.go @@ -153,7 +153,7 @@ func NewCommandParser(commands *CommandSet) *CommandParser { parser.commandNameRE = regexp.MustCompile(fmt.Sprintf(`^(%s+\.)?%s+`, CommandNameTokens, CommandNameTokens)) parser.skipRE = regexp.MustCompile(`^\s*`) parser.terminatorRE = regexp.MustCompile(`^(;|$)`) - parser.addressRE = regexp.MustCompile(`^[1-9A-HJ-NP-Za-km-z]+`) + parser.addressRE = regexp.MustCompile(`^[1-9A-HJ-NP-Za-km-z]+|""`) parser.simpleStringRE = regexp.MustCompile(`^[^\s"\';]+`) parser.amountRE = regexp.MustCompile(`^((\d+(\.\d*)?)|(\.\d+))`) parser.uintRE = regexp.MustCompile(`^[+]?[0-9]+`) @@ -314,6 +314,10 @@ func (p *CommandParser) parseAddress(input []byte) ([]byte, int, error) { return nil, 0, fmt.Errorf("%w", cliutil.ErrInvalidParam) } + if string(m) == "\"\"" { + return make([]byte, 0), 2, nil + } + return m, len(m), nil } diff --git a/internal/cli/token_commands.go b/internal/cli/token_commands.go index acbda5f..71136bc 100644 --- a/internal/cli/token_commands.go +++ b/internal/cli/token_commands.go @@ -8,8 +8,8 @@ import ( "github.com/btcsuite/btcutil/base58" "github.com/koinos/koinos-cli/internal/cliutil" - "github.com/koinos/koinos-proto-golang/v2/koinos/contracts/token" "github.com/koinos/koinos-proto-golang/v2/koinos/protocol" + "github.com/koinos/koinos-proto-golang/v2/koinos/standards/kcs4" util "github.com/koinos/koinos-util-golang/v2" "github.com/shopspring/decimal" "google.golang.org/protobuf/proto" @@ -21,10 +21,13 @@ const ( TokenTotalSupplyEntry = uint32(0xb0da3934) TokenSymbolEntry = uint32(0xb76a7ca1) TokenDecimalsEntry = uint32(0xee80fd2f) + TokenAllowanceEntry = uint32(0x32f09fa1) + TokenAllowancesEntry = uint32(0x8fa16456) + TokenApproveEntry = uint32(0x74e21680) ) func retrieveSymbol(ctx context.Context, client *cliutil.KoinosRPCClient, contractID []byte) (*string, error) { - symbolArguments := token.SymbolArguments{} + symbolArguments := kcs4.SymbolArguments{} args, err := proto.Marshal(&symbolArguments) if err != nil { @@ -36,7 +39,7 @@ func retrieveSymbol(ctx context.Context, client *cliutil.KoinosRPCClient, contra return nil, err } - symbolResult := &token.SymbolResult{} + symbolResult := &kcs4.SymbolResult{} err = proto.Unmarshal(resp.GetResult(), symbolResult) if err != nil { return nil, err @@ -46,7 +49,7 @@ func retrieveSymbol(ctx context.Context, client *cliutil.KoinosRPCClient, contra } func retrieveDecimals(ctx context.Context, client *cliutil.KoinosRPCClient, contractID []byte) (*int, error) { - decimalsArguments := token.DecimalsArguments{} + decimalsArguments := kcs4.DecimalsArguments{} args, err := proto.Marshal(&decimalsArguments) if err != nil { @@ -58,7 +61,7 @@ func retrieveDecimals(ctx context.Context, client *cliutil.KoinosRPCClient, cont return nil, err } - decimalsResult := &token.DecimalsResult{} + decimalsResult := &kcs4.DecimalsResult{} err = proto.Unmarshal(resp.GetResult(), decimalsResult) if err != nil { return nil, err @@ -70,7 +73,7 @@ func retrieveDecimals(ctx context.Context, client *cliutil.KoinosRPCClient, cont } func retrieveBalance(ctx context.Context, client *cliutil.KoinosRPCClient, contractID []byte, address []byte) (*uint64, error) { - balanceOfArguments := token.BalanceOfArguments{} + balanceOfArguments := kcs4.BalanceOfArguments{} balanceOfArguments.Owner = address args, err := proto.Marshal(&balanceOfArguments) @@ -83,7 +86,7 @@ func retrieveBalance(ctx context.Context, client *cliutil.KoinosRPCClient, contr return nil, err } - balanceOfResult := &token.BalanceOfResult{} + balanceOfResult := &kcs4.BalanceOfResult{} err = proto.Unmarshal(resp.GetResult(), balanceOfResult) if err != nil { return nil, err @@ -168,7 +171,25 @@ func (c *RegisterTokenCommand) Execute(ctx context.Context, ee *ExecutionEnviron NewTransferCommand := func(inv *CommandParseResult) Command { return NewTokenTransferCommand(inv, contractID, *precision, *symbol) } - cmd = NewCommandDeclaration(fmt.Sprintf("%s.transfer", c.Name), "Transfers the token", false, NewTransferCommand, *NewCommandArg("to", AddressArg), *NewCommandArg("amount", AmountArg)) + cmd = NewCommandDeclaration(fmt.Sprintf("%s.transfer", c.Name), "Transfers the token", false, NewTransferCommand, *NewCommandArg("to", AddressArg), *NewCommandArg("amount", AmountArg), *NewOptionalCommandArg("memo", StringArg)) + ee.Parser.Commands.AddCommand(cmd) + + NewAllowanceCommand := func(inv *CommandParseResult) Command { + return NewTokenAllowanceCommand(inv, contractID, *precision, *symbol) + } + cmd = NewCommandDeclaration(fmt.Sprintf("%s.allowance", c.Name), "Returns a token allowance", false, NewAllowanceCommand, *NewCommandArg("spender", AddressArg), *NewOptionalCommandArg("owner", AddressArg)) + ee.Parser.Commands.AddCommand(cmd) + + NewAllowancesCommand := func(inv *CommandParseResult) Command { + return NewTokenAllowancesCommand(inv, contractID, *precision, *symbol) + } + cmd = NewCommandDeclaration(fmt.Sprintf("%s.allowances", c.Name), "Returns token allowances", false, NewAllowancesCommand, *NewOptionalCommandArg("start", AddressArg), *NewOptionalCommandArg("limit", UIntArg), *NewOptionalCommandArg("owner", AddressArg)) + ee.Parser.Commands.AddCommand(cmd) + + NewApproveCommand := func(inv *CommandParseResult) Command { + return NewTokenApproveCommand(inv, contractID, *precision, *symbol) + } + cmd = NewCommandDeclaration(fmt.Sprintf("%s.approve", c.Name), "Approves an address to spend token", false, NewApproveCommand, *NewCommandArg("spender", AddressArg), *NewCommandArg("amount", AmountArg), *NewOptionalCommandArg("memo", StringArg)) ee.Parser.Commands.AddCommand(cmd) err = ee.Contracts.Add(c.Name, c.Address, nil, nil) @@ -256,7 +277,7 @@ func (c *TokenTotalSupplyCommand) Execute(ctx context.Context, ee *ExecutionEnvi return nil, fmt.Errorf("%w: cannot check total supply", cliutil.ErrOffline) } - totalSupplyArguments := token.TotalSupplyArguments{} + totalSupplyArguments := kcs4.TotalSupplyArguments{} args, err := proto.Marshal(&totalSupplyArguments) if err != nil { @@ -268,7 +289,7 @@ func (c *TokenTotalSupplyCommand) Execute(ctx context.Context, ee *ExecutionEnvi return nil, err } - totalSupplyResult := &token.TotalSupplyResult{} + totalSupplyResult := &kcs4.TotalSupplyResult{} err = proto.Unmarshal(resp.GetResult(), totalSupplyResult) if err != nil { return nil, err @@ -293,6 +314,7 @@ func (c *TokenTotalSupplyCommand) Execute(ctx context.Context, ee *ExecutionEnvi type TokenTransferCommand struct { Address string Amount string + Memo *string ContractID []byte Precision int Symbol string @@ -300,7 +322,7 @@ type TokenTransferCommand struct { // NewTokenTransferCommand instantiates the command to transfer tokens func NewTokenTransferCommand(inv *CommandParseResult, contractID []byte, precision int, symbol string) Command { - return &TokenTransferCommand{Address: *inv.Args["to"], Amount: *inv.Args["amount"], ContractID: contractID, Precision: precision, Symbol: symbol} + return &TokenTransferCommand{Address: *inv.Args["to"], Amount: *inv.Args["amount"], Memo: inv.Args["memo"], ContractID: contractID, Precision: precision, Symbol: symbol} } // Execute the token transfer @@ -351,12 +373,16 @@ func (c *TokenTransferCommand) Execute(ctx context.Context, ee *ExecutionEnviron return nil, errors.New("could not parse address") } - transferArgs := &token.TransferArguments{ + transferArgs := &kcs4.TransferArguments{ From: walletAddress, To: toAddress, Value: uint64(satoshiAmount), } + if c.Memo != nil { + transferArgs.Memo = c.Memo + } + args, err := proto.Marshal(transferArgs) if err != nil { return nil, err @@ -388,3 +414,248 @@ func (c *TokenTransferCommand) Execute(ctx context.Context, ee *ExecutionEnviron return result, nil } + +// TokenAllowanceCommand is a command that returns a token allowance +type TokenAllowanceCommand struct { + Spender string + Owner *string + ContractID []byte + Precision int + Symbol string +} + +// NewTokenAllowanceCommand instantiates the command to return an allowance +func NewTokenAllowanceCommand(inv *CommandParseResult, contractID []byte, precision int, symbol string) Command { + return &TokenAllowanceCommand{Spender: *inv.Args["spender"], ContractID: contractID, Precision: precision, Symbol: symbol} +} + +// Execute the token allowance +func (c *TokenAllowanceCommand) Execute(ctx context.Context, ee *ExecutionEnvironment) (*ExecutionResult, error) { + if !ee.IsOnline() { + return nil, fmt.Errorf("%w: cannot check allowance", cliutil.ErrOffline) + } + + var owner []byte + if c.Owner == nil { + if !ee.IsWalletOpen() { + return nil, fmt.Errorf("%w: must give an owner address", cliutil.ErrWalletClosed) + } + + owner = ee.Key.AddressBytes() + } else { + owner = base58.Decode(*c.Owner) + if len(owner) == 0 { + return nil, errors.New("could not parse owner address") + } + } + + spender := base58.Decode(c.Spender) + if len(spender) == 0 { + return nil, errors.New("could not parse spender address") + } + + allowanceArguments := kcs4.AllowanceArguments{ + Owner: owner, + Spender: spender, + } + + args, err := proto.Marshal(&allowanceArguments) + if err != nil { + return nil, err + } + + resp, err := ee.RPCClient.ReadContract(ctx, args, c.ContractID, TokenAllowanceEntry) + if err != nil { + return nil, err + } + + allowanceResult := kcs4.AllowanceResult{} + err = proto.Unmarshal(resp.GetResult(), &allowanceResult) + if err != nil { + return nil, err + } + + dec, err := util.SatoshiToDecimal(allowanceResult.Value, c.Precision) + if err != nil { + return nil, err + } + + er := NewExecutionResult() + er.AddMessage(fmt.Sprintf("%s %s", dec, c.Symbol)) + + return er, nil +} + +// AllowancesCommand is a command that returns token allowances +type TokenAllowancesCommand struct { + Start *string + Limit *string + Owner *string + ContractID []byte + Precision int + Symbol string +} + +// NewAllowanceCommand instantiates the command to return an allowance +func NewTokenAllowancesCommand(inv *CommandParseResult, contractID []byte, precision int, symbol string) Command { + return &TokenAllowancesCommand{Start: inv.Args["start"], Limit: inv.Args["limit"], Owner: inv.Args["owner"], ContractID: contractID, Precision: precision, Symbol: symbol} +} + +// Execute the token allowance +func (c *TokenAllowancesCommand) Execute(ctx context.Context, ee *ExecutionEnvironment) (*ExecutionResult, error) { + if !ee.IsOnline() { + return nil, fmt.Errorf("%w: cannot check allowance", cliutil.ErrOffline) + } + + var owner []byte + if c.Owner == nil { + if !ee.IsWalletOpen() { + return nil, fmt.Errorf("%w: must give an owner address", cliutil.ErrWalletClosed) + } + + owner = ee.Key.AddressBytes() + } else { + owner = base58.Decode(*c.Owner) + if len(owner) == 0 { + return nil, errors.New("could not parse owner address") + } + } + + limit := int32(10) + if c.Limit != nil { + limit64, err := strconv.ParseUint(*c.Limit, 10, 31) + if err != nil { + return nil, err + } + + limit = int32(limit64) + } + + getAllowancesArgs := kcs4.GetAllowancesArguments{ + Owner: owner, + Limit: limit, + } + + if c.Start != nil && len(*c.Start) > 0 { + start := base58.Decode(*c.Start) + if len(start) == 0 { + return nil, errors.New("could not parse start address") + } + + getAllowancesArgs.Start = start + } + + args, err := proto.Marshal(&getAllowancesArgs) + if err != nil { + return nil, err + } + + resp, err := ee.RPCClient.ReadContract(ctx, args, c.ContractID, TokenAllowancesEntry) + if err != nil { + return nil, err + } + + getAllowancesResult := kcs4.GetAllowancesResult{} + err = proto.Unmarshal(resp.GetResult(), &getAllowancesResult) + if err != nil { + return nil, err + } + + er := NewExecutionResult() + er.AddMessage("Allowances:") + + for _, allowance := range getAllowancesResult.Allowances { + dec, err := util.SatoshiToDecimal(allowance.Value, c.Precision) + if err != nil { + return nil, err + } + + er.AddMessage(fmt.Sprintf(" - %34s: %s %s", base58.Encode(allowance.Spender), dec, c.Symbol)) + } + + return er, nil +} + +// TokenAllowanceCommand is a command that returns a token allowance +type TokenApproveCommand struct { + Spender string + Amount string + Memo *string + ContractID []byte + Precision int + Symbol string +} + +// NewTokenAllowanceCommand instantiates the command to return an allowance +func NewTokenApproveCommand(inv *CommandParseResult, contractID []byte, precision int, symbol string) Command { + return &TokenApproveCommand{Spender: *inv.Args["spender"], Amount: *inv.Args["amount"], Memo: inv.Args["memo"], ContractID: contractID, Precision: precision, Symbol: symbol} +} + +// Execute the token allowance +func (c *TokenApproveCommand) Execute(ctx context.Context, ee *ExecutionEnvironment) (*ExecutionResult, error) { + if !ee.IsWalletOpen() { + return nil, fmt.Errorf("%w: cannot transfer", cliutil.ErrWalletClosed) + } + + if !ee.IsOnline() && !ee.Session.IsValid() { + return nil, fmt.Errorf("%w: cannot transfer", cliutil.ErrOffline) + } + + decimalAmount, err := decimal.NewFromString(c.Amount) + if err != nil { + return nil, fmt.Errorf("%w: %s", cliutil.ErrInvalidAmount, err.Error()) + } + + satoshiAmount, err := util.DecimalToSatoshi(&decimalAmount, c.Precision) + if err != nil { + return nil, fmt.Errorf("%w: %s", cliutil.ErrInvalidAmount, err.Error()) + } + + walletAddress := ee.Key.AddressBytes() + + spender := base58.Decode(c.Spender) + if len(spender) == 0 { + return nil, errors.New("could not parse spender address") + } + + approveArgs := &kcs4.ApproveArguments{ + Owner: walletAddress, + Spender: spender, + Value: uint64(satoshiAmount), + } + + if c.Memo != nil { + approveArgs.Memo = c.Memo + } + + args, err := proto.Marshal(approveArgs) + if err != nil { + return nil, err + } + + op := &protocol.Operation{ + Op: &protocol.Operation_CallContract{ + CallContract: &protocol.CallContractOperation{ + ContractId: c.ContractID, + EntryPoint: TokenApproveEntry, + Args: args, + }, + }, + } + + result := NewExecutionResult() + result.AddMessage(fmt.Sprintf("Approving %s for %s %s", c.Spender, decimalAmount, c.Symbol)) + + err = ee.Session.AddOperation(op, fmt.Sprintf("Approve %s for %s %s", c.Spender, decimalAmount, c.Symbol)) + if err == nil { + result.AddMessage("Adding operation to transaction session") + } + if err != nil { + err := ee.SubmitTransaction(ctx, result, op) + if err != nil { + return result, fmt.Errorf("cannot transfer, %w", err) + } + } + + return result, nil +}