diff --git a/README.md b/README.md index faa5ffb..172756b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ go get github.com/darrenvechain/thor-go-sdk ## Usage +### Example 1: Creating a New Client + ```golang package main @@ -37,3 +39,75 @@ func main() { fmt.Println(acc.Balance) } ``` + +### Example 2: Interacting with a contract + +```golang +package main + +import ( + "log/slog" + "math/big" + "strings" + + "github.com/darrenvechain/thor-go-sdk/solo" + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/txmanager" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +func main() { + thor, _ := thorgo.FromURL("http://localhost:8669") + + // Load a contract + contractABI, _ := abi.JSON(strings.NewReader(vthoABI)) + vtho := thor.Account(common.HexToAddress("0x0000000000000000000000000000456e65726779")).Contract(&contractABI) + + // Create a delegated transaction manager + origin := txmanager.FromPK(solo.Keys()[0], thor) + gasPayer := txmanager.NewDelegator(solo.Keys()[1]) + txSender := txmanager.NewDelegatedManager(thor, origin, gasPayer) + + // Create a new account to receive the tokens + recipient, _ := txmanager.GeneratePK(thor) + recipientBalance := new(big.Int) + + // Call the balanceOf function + err := vtho.Call("balanceOf", &recipientBalance, recipient.Address()) + slog.Info("recipient balance before", "balance", recipientBalance, "error", err) + + // Send 1000 tokens to the recipient + tx, _ := vtho.Send(txSender, "transfer", recipient.Address(), big.NewInt(1000)) + receipt, _ := tx.Wait() + slog.Info("receipt", "txID", receipt.Meta.TxID, "reverted", receipt.Reverted) + + // Call the balanceOf function again + err = vtho.Call("balanceOf", &recipientBalance, recipient.Address()) + slog.Info("recipient balance after", "balance", recipientBalance, "error", err) +} + +var ( + vthoABI = `[ + { + "constant": true, + "inputs": [{"internalType": "address", "name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + {"internalType": "address", "name": "recipient", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + } + ]` +) +``` diff --git a/builtins/contract.go b/builtins/contract.go new file mode 100644 index 0000000..b8bc24f --- /dev/null +++ b/builtins/contract.go @@ -0,0 +1,74 @@ +package builtins + +import ( + "bytes" + "compress/gzip" + "fmt" + + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/thorgo/accounts" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +type Contract struct { + ABI *abi.ABI + Address common.Address +} + +func (c *Contract) Load(thor *thorgo.Thor) *accounts.Contract { + return thor.Account(c.Address).Contract(c.ABI) +} + +var ( + VTHO = &Contract{ + ABI: mustParseABI(compiledEnergyAbi, "VTHO"), + Address: common.HexToAddress("0x0000000000000000000000000000456e65726779"), + } + Authority = &Contract{ + ABI: mustParseABI(compiledAuthorityAbi, "Authority"), + Address: common.HexToAddress("0x0000000000000000000000417574686f72697479"), + } + Executor = &Contract{ + ABI: mustParseABI(compiledExecutorAbi, "Executor"), + Address: common.HexToAddress("0x0000000000000000000000004578656375746f72"), + } + Extension = &Contract{ + ABI: mustParseABI(compiledExtensionv2Abi, "Extension"), + Address: common.HexToAddress("0x0000000000000000000000457874656e73696f6e"), + } + Prototype = &Contract{ + ABI: mustParseABI(compiledPrototypeAbi, "Prototype"), + Address: common.HexToAddress("0x000000000000000000000050726f746f74797065"), + } + Params = &Contract{ + ABI: mustParseABI(compiledParamsAbi, "Params"), + Address: common.HexToAddress("0x0000000000000000000000000000506172616d73"), + } +) + +func mustParseABI(data []byte, name string) *abi.ABI { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + panic(fmt.Errorf("read %q: %v", name, err)) + } + + contractABI, err := abi.JSON(gz) + if err != nil { + panic(fmt.Errorf("parse %q: %v", name, err)) + } + + return &contractABI +} + +var compiledEnergyAbi = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x56\xcf\xaf\xd3\x30\x0c\xfe\x5f\x7c\xce\x09\x09\x84\x7a\x83\x03\x37\xc4\x01\x6e\x4f\x3b\xb8\xad\x8b\x22\x25\x76\x94\x38\x1b\xd5\xd3\xfb\xdf\xd1\xb6\xac\xab\xa0\xbf\x98\x86\xb6\x53\x2b\xd9\x8e\xbf\xcf\xf6\x97\xf8\xe5\x15\x1a\xe1\xa4\xc8\x0a\x95\xc6\x4c\x06\x2c\x87\xac\x09\xaa\x97\x9d\x01\x46\x4f\x50\x9d\x3f\x06\x24\x6b\x31\xbd\x5e\x2c\x60\x40\xfb\x70\xfc\x4b\x1a\x2d\xff\x84\xb7\x9d\x81\x80\x3d\xd6\x8e\xa0\xea\xd0\x25\x32\x90\x14\x95\xbe\x66\xc5\xda\x3a\xab\x3d\x54\x10\x72\xa4\x6b\x68\x97\xb9\x51\x2b\x0c\x6f\x66\x0c\xa7\x44\x0f\x78\x86\xa4\x29\x10\xb7\x14\xaf\x07\x60\xdb\x46\x4a\xe9\x14\x5f\x7c\xf6\xe8\xf2\x28\x45\xb6\xac\xef\xde\x7f\x38\xc1\x2b\x1e\x18\x42\x94\xfd\x0c\xaf\x94\x9b\xe6\x78\xe2\x70\x40\x2d\xe2\x36\x92\x63\xe1\x8b\xd3\x1a\xc5\xd9\x8a\xab\x28\xba\xef\x39\x04\xd7\xaf\x15\x7e\x4c\x6d\x1d\xdc\xde\xd2\xe1\xf6\xca\x77\x51\xfc\x62\xd9\x55\x16\xcd\xe8\x25\xb3\x2e\xb6\x45\x23\x72\xea\x28\x7e\x39\xa7\x7a\xc2\xde\xb4\xd4\x58\x8f\x2e\x6d\x69\xcc\xc7\x7b\x0a\xe2\x0f\x44\x43\x4a\x39\xf0\xa4\x1a\xae\x90\x6b\x74\xc8\x0d\x7d\xeb\xa6\x31\x17\xf3\x7f\x9d\xa9\xd9\x72\xa6\xde\xd7\xe2\x9e\xe8\x7a\xb9\xdf\x0c\x3f\x74\x7e\x1f\x29\x62\xff\xdc\x17\xeb\xe7\x1c\x99\xda\x07\x5c\xac\xff\xaa\x60\xb3\xe1\xcd\x1b\xbd\x68\xce\xc9\xa1\x08\x79\x82\x59\x24\x8f\x96\x8f\x2a\xba\x3f\x45\x64\xe1\xde\x4b\x4e\x53\x73\x67\xb9\xa5\x5f\xd4\x5e\xe8\xaf\x8e\xe1\xb4\xff\xdc\x54\x0e\xde\x25\xf1\xe6\x05\xe0\xc7\x55\xa5\xc5\x89\xf6\xc4\x7a\x2b\x9f\x85\x1e\x4e\x07\x2c\xae\x31\x37\xb3\xfa\x74\x5a\x6b\xd0\xfd\xc5\x6a\xf7\x3b\x00\x00\xff\xff\x3d\x94\x5b\x7e\xec\x09\x00\x00") + +var compiledAuthorityAbi = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x93\xcd\x6e\xc2\x40\x0c\x84\xdf\xc5\xe7\x3d\xb5\xb7\x5c\x7b\xe6\x09\x10\x07\x13\x0f\xd5\xaa\xc1\x46\xb1\x37\x25\x42\xbc\x7b\x05\x0a\x24\x45\xb4\xa5\x6a\x51\xd5\x5b\x24\xff\xcc\x37\x93\xf5\x7c\x47\xb5\xa9\x07\x6b\x50\x15\x6d\x41\xa2\xac\x9b\x12\x4e\xd5\x7c\x91\x48\x79\x0d\xaa\x68\x95\x5b\x0f\x4a\x64\x25\x86\xda\xee\x54\xa2\x44\xd1\x6f\x0e\x5f\x2c\xd2\xc2\x9d\xf6\x8b\x44\x1b\xee\x79\xd9\x80\xaa\x15\x37\x8e\x44\x1e\x1c\x98\x95\xe0\x65\x6e\x72\xf4\x54\x51\x97\xf1\x3a\xce\xae\x8a\xd6\x91\x4d\x69\x9f\xa6\x40\xc3\xf4\x99\xe8\xac\xaa\x26\x98\xb1\x07\xda\xeb\xfa\x43\x5b\x8b\xce\x5e\xf0\x0e\xfc\x26\x38\x35\x3d\x35\x7d\x85\x78\x91\xd9\x77\x09\x15\xdb\xbf\x08\xf6\x87\xd4\xcf\xf8\x00\xba\xc9\x1e\x90\x71\x76\x69\xd6\x1c\xa5\x87\x3a\x54\xac\x75\xbb\xb6\x7d\x6c\xca\x02\x8d\x83\x99\x71\x4d\x1f\xf0\xc7\x87\x69\x13\xd7\x91\x3b\x5c\x2a\xdd\x2d\xa1\xd1\x3b\xb6\xa8\x4b\x1c\x2d\xfc\x8f\x73\xf8\xbd\xf4\xc7\x10\x58\xe4\x0e\x57\xc5\x6a\xda\xaf\xad\xf8\x35\xab\x59\x05\x5b\xc8\xe9\xcf\xdc\xe8\xfc\x3c\x35\x2c\x9c\xbc\x1e\xd3\x4f\x2d\x3e\xb1\x4a\x16\x8e\x09\x2c\x3a\x68\xd0\x7e\xf1\x16\x00\x00\xff\xff\x5e\x1e\xdc\x95\x35\x05\x00\x00") + +var compiledExecutorAbi = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x55\x3d\x6f\xdb\x40\x0c\xfd\x2f\x9c\x35\xb5\x45\x51\x68\x33\xd2\xa5\x43\x01\x4f\x5d\x82\xc0\xa0\x75\x4c\x7a\xa8\x4c\xaa\x77\x3c\x25\x42\x90\xff\x5e\xc4\xd2\xf5\x24\xeb\xa3\x6e\x6a\x58\xe8\x66\xc0\x8f\x4f\x8f\x8f\x8f\xbc\xdb\x67\x28\x84\xbd\x22\x2b\xe4\xea\x02\x65\x60\xb9\x0a\xea\x21\xbf\x7d\x06\xc6\x03\x41\x0e\x90\x81\x36\xd5\xeb\x2f\x34\xc6\x91\xf7\xf0\x72\x97\xc5\x3f\xb1\xaa\x9c\xd4\xe4\x3c\x64\x20\x41\x4f\x6b\xad\x21\x56\xab\x4d\xe2\xd8\x37\x4a\xfe\xfd\x3b\x78\xc9\x12\x88\xb7\xf2\x48\xae\x87\x11\x29\x8f\x1f\xa9\xb0\xc1\x7d\x49\x90\xdf\x63\xe9\x29\x03\xaf\xa8\xf4\x35\x28\xee\x6d\xf9\xca\x9a\x43\x6d\xe9\x31\x15\xde\x07\x2e\xd4\x0a\x1f\xd9\x67\x3b\x1b\xab\xbf\x91\xc0\x3a\xdd\x41\x22\x0f\x96\xf5\xd3\x25\x65\x75\xd5\x63\xc7\x77\x51\xd7\xa2\xf5\x8e\x6a\xf9\x41\x9b\x04\x4d\xea\xcf\xd2\xc8\xc2\x11\xf4\x97\x06\x4e\x98\xf3\x7b\xac\x49\x5f\xe5\xa4\x12\x8f\xe5\x4c\x34\xd4\x1e\x68\x7b\x84\x90\x19\x9a\xfc\xf1\x43\x3f\x1d\x2d\xcd\xa4\x17\x09\xf4\x33\x88\x0b\x87\xd1\xac\x12\xa0\x75\x14\xcb\x38\xe9\x39\x1c\x3d\x51\x11\xb4\xaf\xa8\x0d\x63\x42\x28\xba\x07\xd2\x45\x35\x06\x15\x4f\xac\x59\x3d\x37\x49\xdd\x6e\x61\x29\x7b\xab\x61\xcc\x55\xa2\x35\xdf\xcc\x19\x46\xef\xe6\x9c\x1e\x66\xe7\x4f\x9b\xdd\x6f\xff\x4a\xcd\x15\xc2\xea\xb0\x98\x6a\xaf\x37\x03\x55\x2c\xbe\x7f\x13\xb5\xfc\x70\x93\x0a\xd6\x18\x46\x5c\xe6\x2f\x9f\x97\x53\xd3\x46\x66\x25\x91\x67\x99\x6a\xe8\x7f\x33\xb5\xbb\x49\x2b\x5f\xf8\x09\x2b\xeb\x81\x89\x33\x77\xfe\xd2\xcf\x3a\xb2\x70\x73\x90\xe0\xa7\x9c\xb5\x6c\xe8\x89\x4c\xec\xe7\xe4\x25\x9a\xf4\x39\xeb\x55\x75\x84\x31\xcc\xed\x47\x97\x46\xb3\xed\x98\x13\x88\x6a\x62\x7d\xab\xd0\xe5\xfb\xfd\x76\x99\x9b\x11\xef\x3f\xc9\x8c\x7b\xb6\x31\xe6\xe2\x52\x47\x8b\x39\x14\x7c\xf7\x2b\x00\x00\xff\xff\xa9\x52\x06\x45\xb3\x0a\x00\x00") + +var compiledExtensionv2Abi = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\xd3\x41\x4b\xc3\x30\x14\x07\xf0\xef\xf2\xce\x39\x55\x1d\xd2\xa3\x28\xe2\x41\x18\x6e\xe0\x61\xf4\xf0\xd2\xbe\x4a\x68\x9a\x84\xe4\x65\x2e\x8c\x7d\x77\x69\x99\x5b\x0f\xe2\x2a\xa2\xed\xad\xd0\xfc\x79\x3f\xfe\xbc\xb7\xd9\x43\x69\x4d\x60\x34\x0c\x39\xfb\x48\x02\x94\x71\x91\x03\xe4\x9b\x42\x80\xc1\x96\x20\x07\xb6\x8c\x7a\x15\x9d\xd3\x09\x04\xd8\xc8\xc7\x17\xfb\xcf\x07\x20\x80\x93\xeb\xbe\xa2\x32\x9c\xdd\x2c\xe0\x50\x08\x70\x98\x50\x6a\x82\xbc\x46\x1d\x48\x40\x60\x64\x7a\x8e\x8c\x52\x69\xc5\x09\x72\xd8\x2a\x7a\x3f\x67\xeb\x68\x4a\x56\xd6\xc0\x41\x7c\xc3\x3a\x0d\xad\x90\xf1\x1c\x96\x89\x29\xf4\x63\x8f\xbf\xa5\xc6\x86\x32\xd9\x61\x2e\x98\xfb\xe8\x55\xf6\x1f\x66\x13\xdb\xaf\xbb\x3a\xa1\x6d\xd9\xac\x55\x4b\x33\xea\x79\x94\x79\xa5\xde\x0c\xf9\x4b\x6a\xac\x2a\x4f\x21\xcc\x46\xbd\xee\x17\xbb\xb4\x7e\x54\xdf\x8b\xeb\x3f\x84\x0f\xae\x6d\xf7\x88\x61\x89\x69\x0e\x75\x0e\x55\x0f\x3b\xa7\x3c\xf6\x99\xc9\x97\x73\xe8\x7a\xba\x9f\xfe\xc0\x87\x9e\xa5\xb7\x5b\xaa\x5e\xad\x6f\xa6\xef\xe9\x67\xe7\x30\xb7\x26\xef\x3a\xd4\x0b\xd5\xa3\x54\xb7\xbf\x44\x15\x1f\x01\x00\x00\xff\xff\x30\xf3\x42\x68\x0c\x07\x00\x00") + +var compiledParamsAbi = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\x92\xc1\x6a\xc3\x30\x0c\x86\xdf\xe5\x3f\xfb\xd4\xb1\x1d\xf2\x0e\x3b\xed\x58\xc2\x50\x12\x75\x98\xa5\x72\x88\xa4\xac\xa6\xe4\xdd\xc7\x4a\x12\xc3\xe8\x08\x65\x47\xe3\xcf\xbf\x3e\xa3\xff\x78\x45\x9b\x44\x8d\xc4\x50\x9d\xa8\x57\x0e\x88\x32\xb8\x29\xaa\xe3\x15\x42\x67\x46\x85\xf7\x4f\xce\x08\xb0\x3c\xfc\x9c\x9a\x6c\xac\x4f\x07\xcc\xa1\x00\x13\xf5\xce\x05\xf1\x28\x76\x78\x7e\xc1\x5c\x87\x15\x51\x36\x04\x24\xb7\x25\xbc\x0e\x18\x28\x53\xd3\xf3\x36\x58\x8d\x8c\x5f\xdd\xa8\x89\x7d\xb4\x8c\x0a\x92\x64\x85\xb6\xec\x93\x4b\x6b\x31\xc9\x6d\x7e\x91\xb7\xd1\x1f\x71\x2f\x62\x1f\xbf\xc4\xb6\x97\xf7\xbf\xb3\x2f\x3d\x45\xfe\x7a\x54\xb7\xe8\xf0\x85\x5b\xb7\x34\xee\x39\x51\xd7\x8d\xac\xfa\x7f\x27\x92\x24\xf9\x9c\x5c\xef\x15\x20\x4a\xc7\x17\xee\x56\xdf\xc5\xe2\xcf\x3a\x6c\xf8\x92\xb4\xf0\xfb\xed\x78\xbb\x2d\x61\xb9\xe7\x89\xc5\x30\xd7\xdf\x01\x00\x00\xff\xff\xfb\x8f\x43\xc8\x9d\x02\x00\x00") + +var compiledPrototypeAbi = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x56\x4d\x8f\xda\x30\x10\xfd\x2f\x3e\xe7\x44\xd5\x1e\x72\x45\xea\x8d\xaa\x6a\xd5\x13\x42\xd5\xc4\x19\xa8\x85\x33\x8e\x3c\x63\x50\x84\xfa\xdf\x57\xa0\x25\x31\x4b\x84\x77\xf9\xd8\x84\x23\xe2\x4d\x3c\xcf\xf3\xde\xf3\xcc\x77\x4a\x3b\x62\x01\x12\x95\x2f\xc1\x32\x66\xca\x50\x1d\x84\x55\x3e\xdf\x29\x82\x0a\x55\xae\xfe\x32\xda\xa5\xca\x94\x34\xf5\xfe\x27\x94\xa5\x47\x66\xf5\x3f\xeb\x10\x84\xdb\x19\xb0\xa0\xef\x81\x2d\xb2\x23\x8c\x51\x5a\x94\x0b\xf2\x7a\xce\x22\x53\x35\x34\x50\x58\x6c\x7b\x60\x01\xc1\x59\x10\x28\x8c\x35\xd2\xa8\x5c\x91\xa3\x23\xa8\x3d\x61\x19\x48\x8b\x71\x74\xe8\xa4\xe3\x21\x3e\x5c\x4b\x23\x70\x82\x81\xe1\x3f\xfc\xa6\xfd\xb6\xba\x2b\x2c\x9c\xb3\x87\xaa\x34\xaf\x8d\xc1\xed\x23\x19\xad\xb1\x89\xfa\x6a\x04\xf9\xcb\xe4\x64\x24\xe2\x3c\xac\xf0\xbb\x4b\x93\x8a\x8a\x87\xe7\x55\x58\xa7\xd7\x3f\x42\x55\xc4\x03\x0b\x86\x64\xf2\xf5\x5b\xcc\x0f\x09\xfd\xaa\x49\x71\x8b\x0b\xef\xc4\xed\x16\x37\x25\x65\xe8\xb1\x72\x1b\x3c\x93\xe2\xd0\x4e\xea\x1a\xd4\xc1\x7b\x24\xf9\x5d\x3b\xe2\xb4\xb4\xe2\x0f\x8c\xe0\xfa\xb5\xc7\xd2\x48\x8f\x3a\x22\x8c\x47\xed\x36\xe8\x9b\x5f\x20\x78\x51\x80\x8c\x32\x3d\x7c\xef\xa7\x05\x7a\xe8\xb4\x6e\xa1\xcc\xed\xa0\x2e\xa5\xb7\x45\xdd\x3b\xd2\xa1\x75\x77\x45\x2e\x14\x60\x81\x34\x3e\x43\x30\x44\x13\x18\xe1\xdd\x47\x9e\xef\x97\xf9\xee\xf4\xff\x8b\xb6\x4a\xbb\x6a\x04\xf1\x90\x4c\x67\x28\xcb\x11\x47\xf3\x3f\xe0\xa9\x2b\x93\xca\x1f\x7a\x87\xe9\x1a\xae\xce\x57\xc6\xcf\x79\x43\x1e\xb9\x48\xee\x01\xd3\xa3\x21\x9e\x28\x82\x02\x8d\x31\x84\x3e\xf8\x92\x19\x7e\xe7\x62\x72\x07\x13\x2c\x5e\x02\x00\x00\xff\xff\xc1\xa7\x82\x58\x64\x0d\x00\x00") diff --git a/client/accounts_test.go b/client/accounts_test.go index fb27a40..fd2d1d8 100644 --- a/client/accounts_test.go +++ b/client/accounts_test.go @@ -19,8 +19,8 @@ func TestClient_Account(t *testing.T) { assert.False(t, acc.HasCode) } -func TestClient_AccountForRevision(t *testing.T) { - acc, err := client.AccountForRevision( +func TestClient_AccountAt(t *testing.T) { + acc, err := client.AccountAt( common.HexToAddress("0xd1d37b8913563fC25BC5bB2E669eB3dBC6b87762"), solo.GenesisID(), ) @@ -37,8 +37,8 @@ func TestClient_AccountCode(t *testing.T) { assert.Greater(t, len(res.Code), 2) } -func TestClient_AccountCodeForRevision(t *testing.T) { - res, err := client.AccountCodeForRevision( +func TestClient_AccountCodeAt(t *testing.T) { + res, err := client.AccountCodeAt( common.HexToAddress("0x0000000000000000000000000000456E65726779"), solo.GenesisID(), ) @@ -56,8 +56,8 @@ func TestClient_AccountStorage(t *testing.T) { assert.Greater(t, len(res.Value), 2) } -func TestClient_AccountStorageForRevision(t *testing.T) { - res, err := client.AccountStorageForRevision( +func TestClient_AccountStorageAt(t *testing.T) { + res, err := client.AccountStorageAt( common.HexToAddress("0x0000000000000000000000000000456E65726779"), common.HexToHash(strings.Repeat("0", 64)), solo.GenesisID(), diff --git a/client/client.go b/client/client.go index 6979a6e..cadba9c 100644 --- a/client/client.go +++ b/client/client.go @@ -12,20 +12,20 @@ import ( ) type Client struct { - client http.Client + client *http.Client url string genesisBlock *Block } -func New(url string, client http.Client) (*Client, error) { +func New(url string, client *http.Client) (*Client, error) { return newClient(url, client) } func FromURL(url string) (*Client, error) { - return New(url, http.Client{}) + return New(url, &http.Client{}) } -func newClient(url string, client http.Client) (*Client, error) { +func newClient(url string, client *http.Client) (*Client, error) { url = strings.TrimSuffix(url, "/") c := &Client{ @@ -44,14 +44,14 @@ func newClient(url string, client http.Client) (*Client, error) { // Account fetches the account information for the given address. func (c *Client) Account(addr common.Address) (*Account, error) { - url := "/accounts/" + addr.String() - return Get(c, url, new(Account)) + url := "/accounts/" + addr.Hex() + return httpGet(c, url, &Account{}) } -// AccountForRevision fetches the account information for the given address at the given revision. -func (c *Client) AccountForRevision(addr common.Address, revision common.Hash) (*Account, error) { - url := "/accounts/" + addr.String() + "?revision=" + revision.String() - return Get(c, url, new(Account)) +// AccountAt fetches the account information for an address at the given revision. +func (c *Client) AccountAt(addr common.Address, revision common.Hash) (*Account, error) { + url := "/accounts/" + addr.Hex() + "?revision=" + revision.Hex() + return httpGet(c, url, &Account{}) } // Inspect will send an array of clauses to the node to simulate the execution of the clauses. @@ -60,45 +60,57 @@ func (c *Client) AccountForRevision(addr common.Address, revision common.Hash) ( // - Simulate the execution of a transaction func (c *Client) Inspect(body InspectRequest) ([]InspectResponse, error) { url := "/accounts/*" - result, err := Post(c, url, body, new([]InspectResponse)) + response := make([]InspectResponse, 0) + _, err := httpPost(c, url, body, &response) if err != nil { return nil, err } - return *result, nil + return response, nil +} + +// InspectAt will send an array of clauses to the node to simulate the execution of the clauses at the given revision. +func (c *Client) InspectAt(body InspectRequest, revision common.Hash) ([]InspectResponse, error) { + url := "/accounts/*?revision=" + revision.Hex() + response := make([]InspectResponse, 0) + _, err := httpPost(c, url, body, &response) + if err != nil { + return nil, err + } + return response, nil } // AccountCode fetches the code for the account at the given address. func (c *Client) AccountCode(addr common.Address) (*AccountCode, error) { - url := "/accounts/" + addr.String() + "/code" - return Get(c, url, new(AccountCode)) + url := "/accounts/" + addr.Hex() + "/code" + return httpGet(c, url, &AccountCode{}) } -// AccountCodeForRevision fetches the code for the account at the given address at the given revision. -func (c *Client) AccountCodeForRevision(addr common.Address, revision common.Hash) (*AccountCode, error) { - url := "/accounts/" + addr.String() + "/code?revision=" + revision.String() - return Get(c, url, new(AccountCode)) +// AccountCodeAt fetches the code for the account at the given address and revision. +func (c *Client) AccountCodeAt(addr common.Address, revision common.Hash) (*AccountCode, error) { + url := "/accounts/" + addr.Hex() + "/code?revision=" + revision.Hex() + return httpGet(c, url, &AccountCode{}) } // AccountStorage fetches the storage value for the account at the given address and key. func (c *Client) AccountStorage(addr common.Address, key common.Hash) (*AccountStorage, error) { - url := "/accounts/" + addr.String() + "/storage/" + key.Hex() - return Get(c, url, new(AccountStorage)) + url := "/accounts/" + addr.Hex() + "/storage/" + key.Hex() + return httpGet(c, url, &AccountStorage{}) } -// AccountStorageForRevision fetches the storage value for the account at the given address and key at the given revision. -func (c *Client) AccountStorageForRevision( +// AccountStorageAt fetches the storage value for the account at the given address and key at the given revision. +func (c *Client) AccountStorageAt( addr common.Address, key common.Hash, revision common.Hash, ) (*AccountStorage, error) { - url := "/accounts/" + addr.Hex() + "/storage/" + key.Hex() + "?revision=" + revision.String() - return Get(c, url, new(AccountStorage)) + url := "/accounts/" + addr.Hex() + "/storage/" + key.Hex() + "?revision=" + revision.Hex() + return httpGet(c, url, &AccountStorage{}) } // Block fetches the block for the given revision. func (c *Client) Block(revision string) (*Block, error) { url := "/blocks/" + revision - return Get(c, url, new(Block)) + return httpGet(c, url, &Block{}) } // BestBlock returns the best block. @@ -114,7 +126,7 @@ func (c *Client) GenesisBlock() *Block { // ExpandedBlock fetches the block at the given revision with all the transactions expanded. func (c *Client) ExpandedBlock(revision string) (*ExpandedBlock, error) { url := "/blocks/" + revision + "?expanded=true" - return Get(c, url, new(ExpandedBlock)) + return httpGet(c, url, &ExpandedBlock{}) } // ChainTag returns the chain tag of the genesis block. @@ -130,87 +142,110 @@ func (c *Client) SendTransaction(tx *transaction.Transaction) (*SendTransactionR return nil, err } body["raw"] = "0x" + encoded - return Post(c, "/transactions", body, new(SendTransactionResponse)) + return httpPost(c, "/transactions", body, &SendTransactionResponse{}) } // SendRawTransaction sends a raw transaction to the node. func (c *Client) SendRawTransaction(raw string) (*SendTransactionResponse, error) { body := make(map[string]string) body["raw"] = raw - return Post(c, "/transactions", body, new(SendTransactionResponse)) + return httpPost(c, "/transactions", body, &SendTransactionResponse{}) } // Transaction fetches a transaction by its ID. func (c *Client) Transaction(id common.Hash) (*Transaction, error) { url := "/transactions/" + id.Hex() - return Get(c, url, new(Transaction)) + return httpGet(c, url, &Transaction{}) +} + +// TransactionAt fetches a transaction by its ID for the given head block ID. +func (c *Client) TransactionAt(id common.Hash, head common.Hash) (*Transaction, error) { + url := "/transactions/" + id.Hex() + "?head=" + head.Hex() + return httpGet(c, url, &Transaction{}) } // RawTransaction fetches a transaction by its ID and returns the raw transaction. func (c *Client) RawTransaction(id common.Hash) (*RawTransaction, error) { url := "/transactions/" + id.Hex() + "?raw=true" - return Get(c, url, new(RawTransaction)) + return httpGet(c, url, &RawTransaction{}) +} + +// RawTransactionAt fetches a transaction by its ID for the given head block ID and returns the raw transaction. +func (c *Client) RawTransactionAt(id common.Hash, head common.Hash) (*RawTransaction, error) { + url := "/transactions/" + id.Hex() + "?head=" + head.Hex() + "&raw=true" + return httpGet(c, url, &RawTransaction{}) } // PendingTransaction includes the pending block when fetching a transaction. func (c *Client) PendingTransaction(id common.Hash) (*Transaction, error) { url := "/transactions/" + id.Hex() + "?pending=true" - return Get(c, url, new(Transaction)) + return httpGet(c, url, &Transaction{}) } +// TransactionReceipt fetches a transaction receipt by its ID. func (c *Client) TransactionReceipt(id common.Hash) (*TransactionReceipt, error) { url := "/transactions/" + id.Hex() + "/receipt" - return Get(c, url, new(TransactionReceipt)) + return httpGet(c, url, &TransactionReceipt{}) +} + +// TransactionReceiptAt fetches a transaction receipt by its ID for the given head block ID. +func (c *Client) TransactionReceiptAt(id common.Hash, head common.Hash) (*TransactionReceipt, error) { + url := "/transactions/" + id.Hex() + "/receipt?revision=" + head.Hex() + return httpGet(c, url, &TransactionReceipt{}) } func (c *Client) FilterEvents(filter *EventFilter) ([]EventLog, error) { path := "/logs/event" - result, err := Post(c, path, filter, new([]EventLog)) + events := make([]EventLog, 0) + _, err := httpPost(c, path, filter, &events) if err != nil { return nil, err } - return *result, nil + return events, nil } func (c *Client) FilterTransfers(filter *TransferFilter) ([]TransferLog, error) { path := "/logs/transfer" - result, err := Post(c, path, filter, new([]TransferLog)) + transfers := make([]TransferLog, 0) + _, err := httpPost(c, path, filter, &transfers) if err != nil { return nil, err } - return *result, nil + return transfers, nil } func (c *Client) Peers() ([]Peer, error) { - result, err := Get(c, "/node/network/peers", new([]Peer)) + path := "/node/network/peers" + peers := make([]Peer, 0) + _, err := httpGet(c, path, &peers) if err != nil { return nil, err } - return *result, nil + return peers, nil } -func Get[T any](c *Client, endpoint string, v *T) (*T, error) { - req, err := http.NewRequest("GET", c.url+endpoint, nil) +func httpGet[T any](c *Client, endpoint string, v *T) (*T, error) { + req, err := http.NewRequest(http.MethodGet, c.url+endpoint, nil) if err != nil { return nil, err } - return Do(c, req, v) + return httpDo(c, req, v) } -func Post[T any](c *Client, path string, body interface{}, v *T) (*T, error) { +func httpPost[T any](c *Client, path string, body interface{}, v *T) (*T, error) { reqBody, err := json.Marshal(body) if err != nil { return nil, err } - request, err := http.NewRequest("POST", c.url+path, strings.NewReader(string(reqBody))) + request, err := http.NewRequest(http.MethodPost, c.url+path, strings.NewReader(string(reqBody))) if err != nil { return nil, err } request.Header.Set("Content-Type", "application/json") - return Do(c, request, v) + return httpDo(c, request, v) } -func Do[T any](c *Client, req *http.Request, v *T) (*T, error) { +func httpDo[T any](c *Client, req *http.Request, v *T) (*T, error) { response, err := c.client.Do(req) if err != nil { return nil, err @@ -222,21 +257,18 @@ func Do[T any](c *Client, req *http.Request, v *T) (*T, error) { defer response.Body.Close() // Read the entire body into a buffer - bodyBytes, err := io.ReadAll(response.Body) + responseBody, err := io.ReadAll(response.Body) if err != nil { return nil, err } // Check if the body is "null" - if strings.TrimSpace(string(bodyBytes)) == "null" { + if strings.TrimSpace(string(responseBody)) == "null" { return nil, ErrNotFound } - // Create a new reader from the buffered body - bodyReader := bytes.NewReader(bodyBytes) - // Decode the JSON response - err = json.NewDecoder(bodyReader).Decode(v) + err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(v) if err != nil { return nil, err } diff --git a/hash/hash_test.go b/hash/hash_test.go index 85011fc..cf10d32 100644 --- a/hash/hash_test.go +++ b/hash/hash_test.go @@ -13,7 +13,6 @@ import ( hash2 "github.com/darrenvechain/thor-go-sdk/hash" "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/assert" "golang.org/x/crypto/sha3" ) diff --git a/solo/solo.go b/solo/solo.go index 8327b5d..c4e3f39 100644 --- a/solo/solo.go +++ b/solo/solo.go @@ -3,7 +3,6 @@ package solo import ( "crypto/ecdsa" - "github.com/darrenvechain/thor-go-sdk/txmanager" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -12,23 +11,31 @@ import ( const URL = "http://localhost:8669" var ( - keys []*ecdsa.PrivateKey + keys = make([]*ecdsa.PrivateKey, 0) genesisId = common.HexToHash("0x00000000c05a20fbca2bf6ae3affba6af4a74b800b585bf7a4988aba7aea69f6") ) -func Keys() []*ecdsa.PrivateKey { - if len(keys) == 0 { - setup() +func init() { + privateKeys := []string{ + "99f0500549792796c14fed62011a51081dc5b5e68fe8bd8a13b86be829c4fd36", + "7b067f53d350f1cf20ec13df416b7b73e88a1dc7331bc904b92108b1e76a08b1", + "f4a1a17039216f535d42ec23732c79943ffb45a089fbb78a14daad0dae93e991", + "35b5cc144faca7d7f220fca7ad3420090861d5231d80eb23e1013426847371c4", + "10c851d8d6c6ed9e6f625742063f292f4cf57c2dbeea8099fa3aca53ef90aef1", + "2dd2c5b5d65913214783a6bd5679d8c6ef29ca9f2e2eae98b4add061d0b85ea0", + "e1b72a1761ae189c10ec3783dd124b902ffd8c6b93cd9ff443d5490ce70047ff", + "35cbc5ac0c3a2de0eb4f230ced958fd6a6c19ed36b5d2b1803a9f11978f96072", + "b639c258292096306d2f60bc1a8da9bc434ad37f15cd44ee9a2526685f592220", + "9d68178cdc934178cca0a0051f40ed46be153cf23cb1805b59cc612c0ad2bbe0", } - return keys -} -func Signers() []*txmanager.PKManager { - var signers []*txmanager.PKManager - for _, key := range Keys() { - signers = append(signers, txmanager.FromPK(key)) + for _, s := range privateKeys { + keys = append(keys, mustParseKey(s)) } - return signers +} + +func Keys() []*ecdsa.PrivateKey { + return keys } // GenesisID returns the genesis block ID. @@ -47,22 +54,3 @@ func mustParseKey(s string) *ecdsa.PrivateKey { } return key } - -func setup() { - privateKeys := []string{ - "99f0500549792796c14fed62011a51081dc5b5e68fe8bd8a13b86be829c4fd36", - "7b067f53d350f1cf20ec13df416b7b73e88a1dc7331bc904b92108b1e76a08b1", - "f4a1a17039216f535d42ec23732c79943ffb45a089fbb78a14daad0dae93e991", - "35b5cc144faca7d7f220fca7ad3420090861d5231d80eb23e1013426847371c4", - "10c851d8d6c6ed9e6f625742063f292f4cf57c2dbeea8099fa3aca53ef90aef1", - "2dd2c5b5d65913214783a6bd5679d8c6ef29ca9f2e2eae98b4add061d0b85ea0", - "e1b72a1761ae189c10ec3783dd124b902ffd8c6b93cd9ff443d5490ce70047ff", - "35cbc5ac0c3a2de0eb4f230ced958fd6a6c19ed36b5d2b1803a9f11978f96072", - "b639c258292096306d2f60bc1a8da9bc434ad37f15cd44ee9a2526685f592220", - "9d68178cdc934178cca0a0051f40ed46be153cf23cb1805b59cc612c0ad2bbe0", - } - - for _, s := range privateKeys { - keys = append(keys, mustParseKey(s)) - } -} diff --git a/thorgo/accounts/accounts.go b/thorgo/accounts/accounts.go index 2319980..072ec81 100644 --- a/thorgo/accounts/accounts.go +++ b/thorgo/accounts/accounts.go @@ -1,7 +1,10 @@ package accounts import ( + "math/big" + "github.com/darrenvechain/thor-go-sdk/client" + "github.com/darrenvechain/thor-go-sdk/transaction" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" ) @@ -27,7 +30,7 @@ func (a *Visitor) Get() (*client.Account, error) { if a.revision == nil { return a.client.Account(a.account) } - return a.client.AccountForRevision(a.account, *a.revision) + return a.client.AccountAt(a.account, *a.revision) } // Code fetches the byte code of the contract at the given address. @@ -36,7 +39,7 @@ func (a *Visitor) Code() (*client.AccountCode, error) { return a.client.AccountCode(a.account) } - return a.client.AccountCodeForRevision(a.account, *a.revision) + return a.client.AccountCodeAt(a.account, *a.revision) } // Storage fetches the storage value for the given key. @@ -45,10 +48,34 @@ func (a *Visitor) Storage(key common.Hash) (*client.AccountStorage, error) { return a.client.AccountStorage(a.account, key) } - return a.client.AccountStorageForRevision(a.account, key, *a.revision) + return a.client.AccountStorageAt(a.account, key, *a.revision) +} + +// Call executes a read-only contract call. +func (a *Visitor) Call(calldata []byte) (*client.InspectResponse, error) { + clause := transaction.NewClause(&a.account).WithData(calldata).WithValue(big.NewInt(0)) + + var inspection []client.InspectResponse + var err error + + if a.revision == nil { + inspection, err = a.client.Inspect(client.InspectRequest{ + Clauses: []*transaction.Clause{clause}, + }) + } else { + inspection, err = a.client.InspectAt(client.InspectRequest{ + Clauses: []*transaction.Clause{clause}, + }, *a.revision) + } + + if err != nil { + return nil, err + } + + return &inspection[0], nil } // Contract returns a new Contract instance. -func (a *Visitor) Contract(abi abi.ABI) *Contract { +func (a *Visitor) Contract(abi *abi.ABI) *Contract { return NewContract(a.client, a.account, abi, a.revision) } diff --git a/thorgo/accounts/accounts_test.go b/thorgo/accounts/accounts_test.go index 73bed71..d444046 100644 --- a/thorgo/accounts/accounts_test.go +++ b/thorgo/accounts/accounts_test.go @@ -1,30 +1,32 @@ -package accounts +package accounts_test import ( - "strings" "testing" + "github.com/darrenvechain/thor-go-sdk/builtins" "github.com/darrenvechain/thor-go-sdk/client" "github.com/darrenvechain/thor-go-sdk/solo" - "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/thorgo/accounts" + "github.com/darrenvechain/thor-go-sdk/txmanager" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" ) var ( - account1 = solo.Signers()[0] - vthoAddr = common.HexToAddress("0x0000000000000000000000000000456e65726779") thorClient, _ = client.FromURL(solo.URL) - vthoABI, _ = abi.JSON(strings.NewReader(erc20ABI)) - vtho = New(thorClient, vthoAddr).Contract(vthoABI) + thor = thorgo.FromClient(thorClient) + vtho = builtins.VTHO + vthoContract = vtho.Load(thor) + account1 = txmanager.FromPK(solo.Keys()[0], thor) ) // TestGetAccount fetches a thor solo account and checks if the balance and energy are greater than 0 func TestGetAccount(t *testing.T) { - acc, err := New(thorClient, account1.Address()).Get() + acc, err := accounts.New(thorClient, account1.Address()).Get() - assert.NoError(t, err, "Account.Get should not return an error") - assert.NotNil(t, acc, "Account.Get should return an account") + assert.NoError(t, err, "Account.httpGet should not return an error") + assert.NotNil(t, acc, "Account.httpGet should return an account") assert.Greater(t, acc.Balance.Uint64(), uint64(0)) assert.Greater(t, acc.Energy.Uint64(), uint64(0)) @@ -34,10 +36,10 @@ func TestGetAccount(t *testing.T) { // TestGetAccountForRevision fetches a thor solo account for the genesis block // and checks if the balance and energy are greater than 0 func TestGetAccountForRevision(t *testing.T) { - acc, err := New(thorClient, account1.Address()).Revision(solo.GenesisID()).Get() + acc, err := accounts.New(thorClient, account1.Address()).Revision(solo.GenesisID()).Get() - assert.NoError(t, err, "Account.Get should not return an error") - assert.NotNil(t, acc, "Account.Get should return an account") + assert.NoError(t, err, "Account.httpGet should not return an error") + assert.NotNil(t, acc, "Account.httpGet should return an account") assert.Greater(t, acc.Balance.Uint64(), uint64(0)) assert.Greater(t, acc.Energy.Uint64(), uint64(0)) @@ -46,7 +48,7 @@ func TestGetAccountForRevision(t *testing.T) { // TestGetCode fetches the code of the VTHO contract and checks if the code length is greater than 2 (0x) func TestGetCode(t *testing.T) { - vtho, err := New(thorClient, vthoAddr).Code() + vtho, err := accounts.New(thorClient, vtho.Address).Code() assert.NoError(t, err, "Account.Code should not return an error") assert.NotNil(t, vtho, "Account.Code should return a code") @@ -55,7 +57,7 @@ func TestGetCode(t *testing.T) { // TestGetCodeForRevision fetches the code of the VTHO contract for the genesis block func TestGetCodeForRevision(t *testing.T) { - vtho, err := New(thorClient, vthoAddr).Revision(solo.GenesisID()).Code() + vtho, err := accounts.New(thorClient, vtho.Address).Revision(solo.GenesisID()).Code() assert.NoError(t, err, "Account.Code should not return an error") assert.NotNil(t, vtho, "Account.Code should return a code") @@ -64,7 +66,7 @@ func TestGetCodeForRevision(t *testing.T) { // TestGetStorage fetches a storage position of the VTHO contract and checks if the value is empty func TestGetStorage(t *testing.T) { - storage, err := New(thorClient, vthoAddr).Storage(common.Hash{}) + storage, err := accounts.New(thorClient, vtho.Address).Storage(common.Hash{}) assert.NoError(t, err, "Account.Storage should not return an error") assert.NotNil(t, storage, "Account.Storage should return a storage") @@ -73,7 +75,7 @@ func TestGetStorage(t *testing.T) { // TestGetStorageForRevision fetches a storage position of the VTHO contract for the genesis block func TestGetStorageForRevision(t *testing.T) { - storage, err := New(thorClient, vthoAddr).Revision(solo.GenesisID()).Storage(common.Hash{}) + storage, err := accounts.New(thorClient, vtho.Address).Revision(solo.GenesisID()).Storage(common.Hash{}) assert.NoError(t, err, "Account.Storage should not return an error") assert.NotNil(t, storage, "Account.Storage should return a storage") diff --git a/thorgo/accounts/contract.go b/thorgo/accounts/contract.go index f53e882..5d3efe2 100644 --- a/thorgo/accounts/contract.go +++ b/thorgo/accounts/contract.go @@ -17,27 +17,27 @@ import ( // Contract represents a smart contract on the blockchain. type Contract struct { client *client.Client - address common.Address revision *common.Hash - abi abi.ABI + ABI *abi.ABI + Address common.Address } func NewContract( client *client.Client, address common.Address, - abi abi.ABI, + abi *abi.ABI, revision *common.Hash, ) *Contract { - return &Contract{client: client, address: address, abi: abi, revision: revision} + return &Contract{client: client, Address: address, ABI: abi, revision: revision} } // Call executes a read-only contract call. func (c *Contract) Call(method string, value interface{}, args ...interface{}) error { - packed, err := c.abi.Pack(method, args...) + packed, err := c.ABI.Pack(method, args...) if err != nil { return fmt.Errorf("failed to pack method %s: %w", method, err) } - clause := transaction.NewClause(&c.address).WithData(packed).WithValue(big.NewInt(0)) + clause := transaction.NewClause(&c.Address).WithData(packed).WithValue(big.NewInt(0)) request := client.InspectRequest{ Clauses: []*transaction.Clause{clause}, } @@ -56,7 +56,7 @@ func (c *Contract) Call(method string, value interface{}, args ...interface{}) e if err != nil { return fmt.Errorf("failed to decode data: %w", err) } - err = c.abi.UnpackIntoInterface(value, method, decoded) + err = c.ABI.UnpackIntoInterface(value, method, decoded) if err != nil { return fmt.Errorf("failed to unpack method %s: %w", method, err) } @@ -67,7 +67,7 @@ func (c *Contract) Call(method string, value interface{}, args ...interface{}) e // The data must include the method signature. func (c *Contract) DecodeCall(data []byte, value interface{}) error { var method string - for name, m := range c.abi.Methods { + for name, m := range c.ABI.Methods { if len(data) >= 4 && bytes.Equal(data[:4], m.ID) { method = name break @@ -80,7 +80,7 @@ func (c *Contract) DecodeCall(data []byte, value interface{}) error { data = data[4:] - err := c.abi.UnpackIntoInterface(value, method, data) + err := c.ABI.UnpackIntoInterface(value, method, data) if err != nil { return fmt.Errorf("failed to unpack method %s: %w", method, err) } @@ -89,49 +89,40 @@ func (c *Contract) DecodeCall(data []byte, value interface{}) error { // AsClause returns a transaction clause for the given method and arguments. func (c *Contract) AsClause(method string, args ...interface{}) (*transaction.Clause, error) { - packed, err := c.abi.Pack(method, args...) + packed, err := c.ABI.Pack(method, args...) if err != nil { return nil, fmt.Errorf("failed to pack method %s: %w", method, err) } - return transaction.NewClause(&c.address).WithData(packed).WithValue(big.NewInt(0)), nil + return transaction.NewClause(&c.Address).WithData(packed).WithValue(big.NewInt(0)), nil } -type TxSigner interface { - SignTransaction(tx *transaction.Transaction) (*transaction.Transaction, error) - Address() common.Address +type TxManager interface { + SendClauses(clauses []*transaction.Clause) (common.Hash, error) } // Send executes a single clause -func (c *Contract) Send(signer TxSigner, method string, args ...interface{}) (*transactions.Visitor, error) { +func (c *Contract) Send(manager TxManager, method string, args ...interface{}) (*transactions.Visitor, error) { clause, err := c.AsClause(method, args...) if err != nil { return &transactions.Visitor{}, fmt.Errorf("failed to pack method %s: %w", method, err) } - tx, err := transactions.NewBuilder(c.client, []*transaction.Clause{clause}, signer.Address()).Build() - if err != nil { - return &transactions.Visitor{}, fmt.Errorf("failed to build transaction: %w", err) - } - tx, err = signer.SignTransaction(tx) - if err != nil { - return &transactions.Visitor{}, fmt.Errorf("failed to sign transaction: %w", err) - } - res, err := c.client.SendTransaction(tx) + txId, err := manager.SendClauses([]*transaction.Clause{clause}) if err != nil { return &transactions.Visitor{}, fmt.Errorf("failed to send transaction: %w", err) } - return transactions.New(c.client, res.ID), nil + return transactions.New(c.client, txId), nil } // EventCriteria returns criteria that can be used to query contract events. // The matchers must be provided in the same order as the event inputs. // Pass nil for values that should be ignored. func (c *Contract) EventCriteria(name string, matchers ...interface{}) (client.EventCriteria, error) { - ev, ok := c.abi.Events[name] + ev, ok := c.ABI.Events[name] if !ok { return client.EventCriteria{}, fmt.Errorf("event %s not found", name) } criteria := client.EventCriteria{ - Address: &c.address, + Address: &c.Address, Topic0: &ev.ID, } @@ -180,7 +171,7 @@ func (c *Contract) DecodeEvents(logs []client.EventLog) ([]Event, error) { continue } - eventABI, err := c.abi.EventByID(log.Topics[0]) + eventABI, err := c.ABI.EventByID(log.Topics[0]) if err != nil { continue } diff --git a/thorgo/accounts/contract_test.go b/thorgo/accounts/contract_test.go index 2303af4..d3012a0 100644 --- a/thorgo/accounts/contract_test.go +++ b/thorgo/accounts/contract_test.go @@ -1,4 +1,4 @@ -package accounts +package accounts_test import ( "math/big" @@ -14,49 +14,49 @@ import ( func TestContract_Call(t *testing.T) { // name var name string - err := vtho.Call("name", &name) + err := vthoContract.Call("name", &name) assert.NoError(t, err) assert.Equal(t, "VeThor", name) // symbol var symbol string - err = vtho.Call("symbol", &symbol) + err = vthoContract.Call("symbol", &symbol) assert.NoError(t, err) assert.Equal(t, "VTHO", symbol) // decimals var decimals uint8 - err = vtho.Call("decimals", &decimals) + err = vthoContract.Call("decimals", &decimals) assert.NoError(t, err) assert.Equal(t, uint8(18), decimals) } func TestContract_DecodeCall(t *testing.T) { - packed, err := vthoABI.Pack("balanceOf", account1.Address()) + packed, err := vtho.ABI.Pack("balanceOf", account1.Address()) assert.NoError(t, err) balance := new(big.Int) - err = vtho.DecodeCall(packed, &balance) + err = vthoContract.DecodeCall(packed, &balance) assert.NoError(t, err) assert.Greater(t, balance.Uint64(), uint64(0)) } func TestContract_AsClause(t *testing.T) { - receiver, err := txmanager.GeneratePK() + receiver, err := txmanager.GeneratePK(thor) assert.NoError(t, err) // transfer clause - clause, err := vtho.AsClause("transfer", receiver.Address(), big.NewInt(1000)) + clause, err := vthoContract.AsClause("transfer", receiver.Address(), big.NewInt(1000)) assert.NoError(t, err) assert.Equal(t, clause.Value(), big.NewInt(0)) - assert.Equal(t, clause.To().Hex(), vthoAddr.Hex()) + assert.Equal(t, clause.To().Hex(), vtho.Address.Hex()) } func TestContract_Send(t *testing.T) { - receiver, err := txmanager.GeneratePK() + receiver, err := txmanager.GeneratePK(thor) assert.NoError(t, err) - tx, err := vtho.Send(account1, "transfer", receiver.Address(), big.NewInt(1000)) + tx, err := vthoContract.Send(account1, "transfer", receiver.Address(), big.NewInt(1000)) assert.NoError(t, err) receipt, err := tx.Wait() @@ -65,17 +65,17 @@ func TestContract_Send(t *testing.T) { } func TestContract_EventCriteria(t *testing.T) { - receiver, err := txmanager.GeneratePK() + receiver, err := txmanager.GeneratePK(thor) assert.NoError(t, err) - tx, err := vtho.Send(account1, "transfer", receiver.Address(), big.NewInt(1000)) + tx, err := vthoContract.Send(account1, "transfer", receiver.Address(), big.NewInt(1000)) assert.NoError(t, err) receipt, _ := tx.Wait() assert.False(t, receipt.Reverted) // event criteria - match the newly created receiver - criteria, err := vtho.EventCriteria("Transfer", nil, receiver.Address()) + criteria, err := vthoContract.EventCriteria("Transfer", nil, receiver.Address()) assert.NoError(t, err) // fetch events @@ -83,7 +83,7 @@ func TestContract_EventCriteria(t *testing.T) { assert.NoError(t, err) // decode events - decodedEvs, err := vtho.DecodeEvents(transfers) + decodedEvs, err := vthoContract.DecodeEvents(transfers) assert.NoError(t, err) ev := decodedEvs[0] @@ -95,112 +95,3 @@ func TestContract_EventCriteria(t *testing.T) { assert.IsType(t, common.Address{}, ev.Args["to"]) assert.IsType(t, &big.Int{}, ev.Args["value"]) } - -const erc20ABI = `[ - { - "constant":true, - "inputs":[], - "name":"name", - "outputs":[ - { - "name":"", - "type":"string" - } - ], - "payable":false, - "stateMutability":"view", - "type":"function" - }, - { - "constant":true, - "inputs":[], - "name":"symbol", - "outputs":[ - { - "name":"", - "type":"string" - } - ], - "payable":false, - "stateMutability":"view", - "type":"function" - }, - { - "constant":true, - "inputs":[], - "name":"decimals", - "outputs":[ - { - "name":"", - "type":"uint8" - } - ], - "payable":false, - "stateMutability":"view", - "type":"function" - }, - { - "constant":false, - "inputs":[ - { - "name":"to", - "type":"address" - }, - { - "name":"value", - "type":"uint256" - } - ], - "name":"transfer", - "outputs":[ - { - "name":"", - "type":"bool" - } - ], - "payable":false, - "stateMutability":"nonpayable", - "type":"function" - }, - { - "constant":true, - "name":"balanceOf", - "inputs":[ - { - "name":"owner", - "type":"address" - } - ], - "outputs":[ - { - "name":"", - "type":"uint256" - } - ], - "payable":false, - "stateMutability":"view", - "type":"function" - }, - { - "anonymous":false, - "inputs":[ - { - "indexed":true, - "name":"from", - "type":"address" - }, - { - "indexed":true, - "name":"to", - "type":"address" - }, - { - "indexed":false, - "name":"value", - "type":"uint256" - } - ], - "name":"Transfer", - "type":"event" - } -]` diff --git a/thorgo/events/events.go b/thorgo/events/events.go index 653b085..7ce6a24 100644 --- a/thorgo/events/events.go +++ b/thorgo/events/events.go @@ -1,8 +1,6 @@ package events import ( - "errors" - "github.com/darrenvechain/thor-go-sdk/client" ) @@ -53,10 +51,6 @@ func (f *Filter) TimeRange(from uint64, to uint64) *Filter { } func (f *Filter) Apply(offset uint64, limit uint64) ([]client.EventLog, error) { - if limit > 256 { - return nil, errors.New("limit must be less than or equal to 256") - } - f.request.Options = &client.FilterOptions{ Offset: &offset, Limit: &limit, diff --git a/thorgo/thorgo.go b/thorgo/thorgo.go index 027b692..a62a616 100644 --- a/thorgo/thorgo.go +++ b/thorgo/thorgo.go @@ -12,8 +12,8 @@ import ( ) type Thor struct { - client *client.Client Blocks *blocks.Blocks + Client *client.Client } func FromURL(url string) (*Thor, error) { @@ -22,33 +22,29 @@ func FromURL(url string) (*Thor, error) { return nil, err } - return &Thor{client: c, Blocks: blocks.New(c)}, nil + return &Thor{Client: c, Blocks: blocks.New(c)}, nil } func FromClient(c *client.Client) *Thor { - return &Thor{client: c, Blocks: blocks.New(c)} + return &Thor{Client: c, Blocks: blocks.New(c)} } func (t *Thor) Account(address common.Address) *accounts.Visitor { - return accounts.New(t.client, address) + return accounts.New(t.Client, address) } func (t *Thor) Transaction(hash common.Hash) *transactions.Visitor { - return transactions.New(t.client, hash) + return transactions.New(t.Client, hash) } func (t *Thor) TxBuilder(clauses []*transaction.Clause, caller common.Address) *transactions.Builder { - return transactions.NewBuilder(t.client, clauses, caller) + return transactions.NewBuilder(t.Client, clauses, caller) } func (t *Thor) Events(criteria []client.EventCriteria) *events.Filter { - return events.New(t.client, criteria) + return events.New(t.Client, criteria) } func (t *Thor) Transfers(criteria []client.TransferCriteria) *transfers.Filter { - return transfers.New(t.client, criteria) -} - -func (t *Thor) Client() *client.Client { - return t.client + return transfers.New(t.Client, criteria) } diff --git a/thorgo/thorgo_test.go b/thorgo/thorgo_test.go index 185cb06..e55c716 100644 --- a/thorgo/thorgo_test.go +++ b/thorgo/thorgo_test.go @@ -29,7 +29,7 @@ func TestFromClient(t *testing.T) { c, _ := client.FromURL(solo.URL) thor := FromClient(c) assert.NotNil(t, thor) - assert.Equal(t, solo.ChainTag(), thor.Client().ChainTag()) + assert.Equal(t, solo.ChainTag(), thor.Client.ChainTag()) } func TestBlock(t *testing.T) { @@ -41,8 +41,8 @@ func TestBlock(t *testing.T) { func TestGetAccount(t *testing.T) { soloAccount := common.HexToAddress("0xf077b491b355E64048cE21E3A6Fc4751eEeA77fa") acc, err := thor.Account(soloAccount).Get() - assert.NoError(t, err, "Account.Get should not return an error") - assert.NotNil(t, acc, "Account.Get should return an account") + assert.NoError(t, err, "Account.httpGet should not return an error") + assert.NotNil(t, acc, "Account.httpGet should return an account") assert.Greater(t, acc.Balance.Uint64(), uint64(0)) assert.Greater(t, acc.Energy.Uint64(), uint64(0)) diff --git a/thorgo/transactions/transactions_delegation_test.go b/thorgo/transactions/transactions_delegation_test.go deleted file mode 100644 index 8c9c0a0..0000000 --- a/thorgo/transactions/transactions_delegation_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package transactions - -import ( - "math/big" - "testing" - - "github.com/darrenvechain/thor-go-sdk/client" - "github.com/darrenvechain/thor-go-sdk/solo" - "github.com/darrenvechain/thor-go-sdk/transaction" - "github.com/stretchr/testify/assert" -) - -var ( - account1 = solo.Signers()[0] - account2 = solo.Signers()[1] - account3 = solo.Signers()[2] - thorClient, _ = client.FromURL(solo.URL) -) - -func TestDelegation(t *testing.T) { - to := account3.Address() - clause := transaction.NewClause(&to).WithValue(big.NewInt(1000)) - tx, err := NewBuilder(thorClient, []*transaction.Clause{clause}, account1.Address()). - Delegate(). - Build() - assert.NoError(t, err) - - delegatorSig, err := account2.DelegateTransaction(tx, account1.Address()) - assert.NoError(t, err) - tx, err = account1.SignDelegated(tx, delegatorSig) - assert.NoError(t, err) - - res, err := thorClient.SendTransaction(tx) - assert.NoError(t, err) - - receipt, err := New(thorClient, res.ID).Wait() - assert.NoError(t, err) - assert.False(t, receipt.Reverted) - - // Check if the transaction was delegated - assert.Equal(t, receipt.GasPayer, account2.Address()) - assert.Equal(t, receipt.Meta.TxOrigin, account1.Address()) -} diff --git a/thorgo/transactions/transactions_test.go b/thorgo/transactions/transactions_test.go index 87058e5..c6311bf 100644 --- a/thorgo/transactions/transactions_test.go +++ b/thorgo/transactions/transactions_test.go @@ -1,30 +1,43 @@ -package transactions +package transactions_test import ( "math/big" "testing" + "github.com/darrenvechain/thor-go-sdk/client" + "github.com/darrenvechain/thor-go-sdk/solo" + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/thorgo/transactions" "github.com/darrenvechain/thor-go-sdk/transaction" + "github.com/darrenvechain/thor-go-sdk/txmanager" "github.com/stretchr/testify/assert" ) +var ( + thorClient, _ = client.FromURL(solo.URL) + thor = thorgo.FromClient(thorClient) + account1 = txmanager.FromPK(solo.Keys()[0], thor) + account2 = txmanager.FromPK(solo.Keys()[1], thor) +) + // TestTransactions demonstrates how to build, sign, send, and wait for a transaction func TestTransactions(t *testing.T) { // build a transaction to := account2.Address() vetClause := transaction.NewClause(&to).WithValue(big.NewInt(1000)) - unsigned, err := NewBuilder(thorClient, []*transaction.Clause{vetClause}, account1.Address()).Build() + unsigned, err := transactions.NewBuilder(thorClient, []*transaction.Clause{vetClause}, account1.Address()).Build() assert.NoError(t, err) // sign it - signed, err := account1.SignTransaction(unsigned) + signature, err := account1.SignTransaction(unsigned) assert.NoError(t, err) + signed := unsigned.WithSignature(signature) // send it res, err := thorClient.SendTransaction(signed) assert.NoError(t, err) - tx := New(thorClient, res.ID) + tx := transactions.New(thorClient, res.ID) // fetch the pending transaction pending, err := tx.Pending() diff --git a/thorgo/transactions/txbuilder.go b/thorgo/transactions/txbuilder.go index 29f64fb..d13f152 100644 --- a/thorgo/transactions/txbuilder.go +++ b/thorgo/transactions/txbuilder.go @@ -9,10 +9,11 @@ import ( ) type Builder struct { - client *client.Client - clauses []*transaction.Clause - builder *transaction.Builder - caller common.Address + client *client.Client + clauses []*transaction.Clause + builder *transaction.Builder + caller common.Address + gasPayer *common.Address } func NewBuilder(client *client.Client, clauses []*transaction.Clause, caller common.Address) *Builder { @@ -25,6 +26,12 @@ func NewBuilder(client *client.Client, clauses []*transaction.Clause, caller com } } +// GasPayer sets the gas payer for the transaction. This is used to simulate the transaction. +func (b *Builder) GasPayer(payer common.Address) *Builder { + b.gasPayer = &payer + return b +} + // Gas sets the gas provision for the transaction. If not set, it will be estimated. func (b *Builder) Gas(gas uint64) *Builder { b.builder.Gas(gas) @@ -61,7 +68,7 @@ func (b *Builder) DependsOn(txID *common.Hash) *Builder { return b } -// Delegated enables transaction delegation. If not set, it will be nil. +// Delegate enables transaction delegation. If not set, it will be nil. func (b *Builder) Delegate() *Builder { b.builder.Features(transaction.DelegationFeature) return b @@ -74,6 +81,10 @@ func (b *Builder) Simulate() (Simulation, error) { Caller: &b.caller, } + if b.gasPayer != nil { + request.GasPayer = b.gasPayer + } + response, err := b.client.Inspect(request) if err != nil { return Simulation{}, err diff --git a/thorgo/transactions/txbuilder_test.go b/thorgo/transactions/txbuilder_test.go index e0f7014..aa61084 100644 --- a/thorgo/transactions/txbuilder_test.go +++ b/thorgo/transactions/txbuilder_test.go @@ -1,10 +1,11 @@ -package transactions +package transactions_test import ( "math/big" "testing" "github.com/darrenvechain/thor-go-sdk/solo" + "github.com/darrenvechain/thor-go-sdk/thorgo/transactions" "github.com/darrenvechain/thor-go-sdk/transaction" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" @@ -21,7 +22,7 @@ func TestContractClause(t *testing.T) { // transfer clause clause := transaction.NewClause(&account2Addr).WithData([]byte{}).WithValue(big.NewInt(1000)) - txbuilder := NewBuilder(thorClient, []*transaction.Clause{clause}, account1Addr) + txbuilder := transactions.NewBuilder(thorClient, []*transaction.Clause{clause}, account1Addr) // simulation simulation, err := txbuilder.Simulate() diff --git a/transaction/transaction.go b/transaction/transaction.go index 685030b..33fc22d 100644 --- a/transaction/transaction.go +++ b/transaction/transaction.go @@ -17,7 +17,6 @@ import ( "github.com/darrenvechain/thor-go-sdk/hash" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/secp256k1" @@ -452,6 +451,14 @@ func (t *Transaction) validateSignatureLength() error { return nil } +func Decode(data []byte) (*Transaction, error) { + var tx Transaction + if err := rlp.DecodeBytes(data, &tx); err != nil { + return nil, err + } + return &tx, nil +} + // IntrinsicGas calculate intrinsic gas cost for tx with such clauses. func IntrinsicGas(clauses ...*Clause) (uint64, error) { if len(clauses) == 0 { diff --git a/txmanager/delegator.go b/txmanager/delegator.go new file mode 100644 index 0000000..fa12ebe --- /dev/null +++ b/txmanager/delegator.go @@ -0,0 +1,122 @@ +package txmanager + +import ( + "bytes" + "crypto/ecdsa" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/transaction" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// DelegatedManager is a transaction manager that delegates the payment of transaction fees to a Delegator +type DelegatedManager struct { + thor *thorgo.Thor + gasPayer Delegator + origin Signer +} + +func NewDelegatedManager(thor *thorgo.Thor, origin Signer, gasPayer Delegator) *DelegatedManager { + return &DelegatedManager{ + thor: thor, + origin: origin, + gasPayer: gasPayer, + } +} + +func (d *DelegatedManager) SendClauses(clauses []*transaction.Clause) (common.Hash, error) { + tx, err := d.thor.TxBuilder(clauses, d.Address()).Delegate().Build() + if err != nil { + return common.Hash{}, fmt.Errorf("failed to build transaction: %w", err) + } + delegatorSig, err := d.gasPayer.Delegate(tx, d.Address()) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to delegate: %w", err) + } + signature, err := d.origin.SignTransaction(tx) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to sign transaction: %w", err) + } + + signature = append(signature, delegatorSig...) + tx = tx.WithSignature(signature) + res, err := d.thor.Client.SendTransaction(tx) + if err != nil { + return common.Hash{}, fmt.Errorf("failed to send transaction: %w", err) + } + return res.ID, nil +} + +// Address returns the address of the origin manager +func (d *DelegatedManager) Address() common.Address { + return d.origin.Address() +} + +// PKDelegator is a delegator that uses a private key to pay for transaction fees +type PKDelegator struct { + key *ecdsa.PrivateKey +} + +func NewDelegator(key *ecdsa.PrivateKey) *PKDelegator { + return &PKDelegator{key: key} +} + +func (p *PKDelegator) PublicKey() *ecdsa.PublicKey { + return &p.key.PublicKey +} + +func (p *PKDelegator) Address() (addr common.Address) { + return crypto.PubkeyToAddress(p.key.PublicKey) +} + +func (p *PKDelegator) Delegate(tx *transaction.Transaction, origin common.Address) ([]byte, error) { + return crypto.Sign(tx.DelegatorSigningHash(origin).Bytes(), p.key) +} + +// URLDelegator is a delegator that uses a remote URL to pay for transaction fees +type URLDelegator struct { + url string +} + +func NewUrlDelegator(url string) *URLDelegator { + return &URLDelegator{url: url} +} + +func (p *URLDelegator) Delegate(tx *transaction.Transaction, origin common.Address) ([]byte, error) { + encoded, err := tx.Encoded() + if err != nil { + return nil, err + } + + req := &DelegateRequest{ + Origin: origin.String(), + Raw: encoded, + } + + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + + res, err := http.Post(p.url, "application/json", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + if res.StatusCode != http.StatusOK { + return nil, errors.New("200 OK expected") + } + + defer res.Body.Close() + var response DelegateResponse + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } + + return common.FromHex(response.Signature), nil +} diff --git a/txmanager/delegator_test.go b/txmanager/delegator_test.go new file mode 100644 index 0000000..b7e5c3e --- /dev/null +++ b/txmanager/delegator_test.go @@ -0,0 +1,132 @@ +package txmanager + +import ( + "crypto/ecdsa" + "encoding/json" + "math/big" + "net/http" + "net/http/httptest" + "testing" + + "github.com/darrenvechain/thor-go-sdk/builtins" + "github.com/darrenvechain/thor-go-sdk/solo" + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/transaction" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" +) + +var ( + // DelegatedManager should implement Manager + _ Manager = &DelegatedManager{} + // PKDelegator should implement Delegator + _ Delegator = &PKDelegator{} + // URLDelegator should implement Delegator + _ Delegator = &URLDelegator{} +) + +func TestPKDelegator(t *testing.T) { + origin := FromPK(solo.Keys()[0], thor) + delegator := NewDelegator(solo.Keys()[1]) + + clause := transaction.NewClause(&common.Address{}).WithValue(new(big.Int)) + tx, err := thor.TxBuilder([]*transaction.Clause{clause}, origin.Address()).Delegate().Build() + assert.NoError(t, err) + + delegatorSignature, err := delegator.Delegate(tx, origin.Address()) + assert.NoError(t, err) + + signature, err := origin.SignTransaction(tx) + assert.NoError(t, err) + signature = append(signature, delegatorSignature...) + + signedTx := tx.WithSignature(signature) + originAddr, err := signedTx.Origin() + assert.NoError(t, err) + assert.Equal(t, origin.Address(), originAddr) + delegatorAddr, err := signedTx.Delegator() + assert.NoError(t, err) + assert.Equal(t, delegator.Address(), *delegatorAddr) +} + +func createDelegationServer(key *ecdsa.PrivateKey) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req DelegateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + tx, err := transaction.Decode(common.Hex2Bytes(req.Raw)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + origin := common.HexToAddress(req.Origin) + signingHash := tx.DelegatorSigningHash(origin) + signature, err := crypto.Sign(signingHash.Bytes(), key) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + resp := DelegateResponse{Signature: common.Bytes2Hex(signature)} + if err := json.NewEncoder(w).Encode(resp); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) +} + +func TestNewUrlDelegator(t *testing.T) { + origin := FromPK(solo.Keys()[0], thor) + server := createDelegationServer(solo.Keys()[1]) + + delegator := NewUrlDelegator(server.URL) + + clause := transaction.NewClause(&common.Address{}).WithValue(new(big.Int)) + tx, err := thor.TxBuilder([]*transaction.Clause{clause}, origin.Address()).Delegate().Build() + assert.NoError(t, err) + + delegatorSignature, err := delegator.Delegate(tx, origin.Address()) + assert.NoError(t, err) + + signature, err := origin.SignTransaction(tx) + assert.NoError(t, err) + + signature = append(signature, delegatorSignature...) + + //combine the 2 signatures to make 1 of 130 bytes + signed := tx.WithSignature(signature) + + //verify the signature + delegatorAddr, err := signed.Delegator() + assert.NoError(t, err) + assert.Equal(t, *delegatorAddr, crypto.PubkeyToAddress(solo.Keys()[1].PublicKey)) + originAddr, err := signed.Origin() + assert.NoError(t, err) + assert.Equal(t, originAddr, origin.Address()) +} + +func TestNewDelegatedManager(t *testing.T) { + thor, err := thorgo.FromURL("http://localhost:8669") + assert.NoError(t, err) + + origin := FromPK(solo.Keys()[0], thor) + gasPayer := NewDelegator(solo.Keys()[1]) + manager := NewDelegatedManager(thor, origin, gasPayer) + + contract := builtins.VTHO.Load(thor) + + tx, err := contract.Send(manager, "transfer", common.Address{100}, big.NewInt(1000)) + assert.NoError(t, err) + + receipt, err := tx.Wait() + assert.NoError(t, err) + assert.False(t, receipt.Reverted) + + assert.Equal(t, gasPayer.Address().Hex(), receipt.GasPayer.Hex()) + assert.Equal(t, origin.Address().Hex(), receipt.Meta.TxOrigin.Hex()) +} diff --git a/txmanager/pk_manager.go b/txmanager/pk_manager.go deleted file mode 100644 index 5d1131b..0000000 --- a/txmanager/pk_manager.go +++ /dev/null @@ -1,76 +0,0 @@ -package txmanager - -import ( - "crypto/ecdsa" - "errors" - - "github.com/darrenvechain/thor-go-sdk/transaction" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" -) - -type PKManager struct { - key *ecdsa.PrivateKey -} - -func FromPK(key *ecdsa.PrivateKey) *PKManager { - return &PKManager{key: key} -} - -func GeneratePK() (*PKManager, error) { - key, err := crypto.GenerateKey() - if err != nil { - return nil, err - } - - return &PKManager{key: key}, nil -} - -func (p *PKManager) Address() (addr common.Address) { - return crypto.PubkeyToAddress(p.key.PublicKey) -} - -func (p *PKManager) PublicKey() *ecdsa.PublicKey { - return &p.key.PublicKey -} - -func (p *PKManager) SignTransaction(tx *transaction.Transaction) (*transaction.Transaction, error) { - var signingHash common.Hash - - if tx.Features().IsDelegated() { - signingHash = tx.DelegatorSigningHash(p.Address()) - } else { - signingHash = tx.SigningHash() - } - - signature, err := crypto.Sign(signingHash.Bytes(), p.key) - if err != nil { - return nil, err - } - - return tx.WithSignature(signature), nil -} - -func (p *PKManager) SignDelegated(tx *transaction.Transaction, delegatorSig []byte) (*transaction.Transaction, error) { - if !tx.Features().IsDelegated() { - return nil, errors.New("cannot sign non-delegated transaction") - } - - signature, err := crypto.Sign(tx.SigningHash().Bytes(), p.key) - if err != nil { - return nil, err - } - - // signature = originSig + delegatorSig - signature = append(signature, delegatorSig...) - - return tx.WithSignature(signature), nil -} - -func (p *PKManager) DelegateTransaction(tx *transaction.Transaction, origin common.Address) ([]byte, error) { - if !tx.Features().IsDelegated() { - return nil, errors.New("cannot sign non-delegated transaction") - } - - return crypto.Sign(tx.DelegatorSigningHash(origin).Bytes(), p.key) -} diff --git a/txmanager/private_key.go b/txmanager/private_key.go new file mode 100644 index 0000000..b7600d7 --- /dev/null +++ b/txmanager/private_key.go @@ -0,0 +1,61 @@ +package txmanager + +import ( + "crypto/ecdsa" + + "github.com/darrenvechain/thor-go-sdk/thorgo" + "github.com/darrenvechain/thor-go-sdk/transaction" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +type PKManager struct { + key *ecdsa.PrivateKey + thor *thorgo.Thor +} + +func FromPK(key *ecdsa.PrivateKey, thor *thorgo.Thor) *PKManager { + return &PKManager{key: key, thor: thor} +} + +func GeneratePK(thor *thorgo.Thor) (*PKManager, error) { + key, err := crypto.GenerateKey() + if err != nil { + return nil, err + } + + return &PKManager{key: key, thor: thor}, nil +} + +func (p *PKManager) Address() (addr common.Address) { + return crypto.PubkeyToAddress(p.key.PublicKey) +} + +func (p *PKManager) PublicKey() *ecdsa.PublicKey { + return &p.key.PublicKey +} + +func (p *PKManager) SendClauses(clauses []*transaction.Clause) (common.Hash, error) { + tx, err := p.thor.TxBuilder(clauses, p.Address()).Build() + if err != nil { + return common.Hash{}, err + } + signature, err := p.SignTransaction(tx) + if err != nil { + return common.Hash{}, err + } + res, err := p.thor.Client.SendTransaction(tx.WithSignature(signature)) + if err != nil { + return common.Hash{}, err + } + return res.ID, nil +} + +func (p *PKManager) SignTransaction(tx *transaction.Transaction) ([]byte, error) { + signature, err := crypto.Sign(tx.SigningHash().Bytes(), p.key) + if err != nil { + return nil, err + } + + return signature, nil +} diff --git a/txmanager/pk_manager_test.go b/txmanager/private_key_test.go similarity index 64% rename from txmanager/pk_manager_test.go rename to txmanager/private_key_test.go index 5318f4d..2a16e6f 100644 --- a/txmanager/pk_manager_test.go +++ b/txmanager/private_key_test.go @@ -4,16 +4,28 @@ import ( "math/big" "testing" + "github.com/darrenvechain/thor-go-sdk/thorgo" "github.com/darrenvechain/thor-go-sdk/transaction" "github.com/stretchr/testify/assert" ) +var ( + thor, _ = thorgo.FromURL("http://localhost:8669") +) + +var ( + // PKManager should implement Manager + _ Manager = &PKManager{} + // PKManager should implement Signer + _ Signer = &PKManager{} +) + // TestPKSigner demonstrates ease the ease of sending a transaction using a private key signer func TestPKSigner(t *testing.T) { - signer, err := GeneratePK() + signer, err := GeneratePK(thor) assert.NoError(t, err) - to, err := GeneratePK() + to, err := GeneratePK(thor) assert.NoError(t, err) toAddr := to.Address() vetClause := transaction.NewClause(&toAddr).WithValue(big.NewInt(1000)) @@ -26,8 +38,9 @@ func TestPKSigner(t *testing.T) { BlockRef(transaction.NewBlockRef(100)). Build() - signedTx, err := signer.SignTransaction(tx) + signature, err := signer.SignTransaction(tx) assert.NoError(t, err) + signedTx := tx.WithSignature(signature) origin, err := signedTx.Origin() assert.NoError(t, err) assert.Equal(t, signer.Address(), origin) diff --git a/txmanager/types.go b/txmanager/types.go new file mode 100644 index 0000000..4736624 --- /dev/null +++ b/txmanager/types.go @@ -0,0 +1,42 @@ +package txmanager + +import ( + "github.com/darrenvechain/thor-go-sdk/transaction" + "github.com/ethereum/go-ethereum/common" +) + +type Options struct { + ChainTag uint8 + BlockRef string + Expiration uint + Gas uint64 + GasPrice uint8 + Nonce uint64 + DependsOn common.Hash +} + +// Manager represents a transaction manager. It is used to send transactions to the blockchain +type Manager interface { + Address() common.Address + SendClauses(clauses []*transaction.Clause) (common.Hash, error) +} + +// Signer is used for signing transactions +type Signer interface { + Address() common.Address + SignTransaction(tx *transaction.Transaction) ([]byte, error) +} + +// Delegator handles the payment of transaction fees +type Delegator interface { + Delegate(tx *transaction.Transaction, origin common.Address) ([]byte, error) +} + +type DelegateRequest struct { + Origin string `json:"origin"` + Raw string `json:"raw"` +} + +type DelegateResponse struct { + Signature string `json:"signature"` +}