diff --git a/library/build.gradle b/library/build.gradle index 0636ee9..ee9a6a8 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -15,7 +15,6 @@ configurations { } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" diff --git a/library/go/.gitignore b/library/go/.gitignore deleted file mode 100644 index d163863..0000000 --- a/library/go/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ \ No newline at end of file diff --git a/library/go/build-darwin.sh b/library/go/build-darwin.sh deleted file mode 100755 index 630b33c..0000000 --- a/library/go/build-darwin.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# should install this version -#go install github.com/tougee/jvm/cmd/gomobile -#go install github.com/tougee/jvm/cmd/gobind - -# should export JAVA_HOME if not set -export JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home - -gomobile bind -target=darwin/amd64 -x -v -o build -tags=openssl mixin/kernel \ No newline at end of file diff --git a/library/go/build-linux.sh b/library/go/build-linux.sh deleted file mode 100755 index 6557778..0000000 --- a/library/go/build-linux.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# should install this version -#go install github.com/tougee/jvm/cmd/gomobile -#go install github.com/tougee/jvm/cmd/gobind - -# build on MacOS -# brew install FiloSottile/musl-cross/musl-cross -# export CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++ -# export JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home - -# build on Linux -# export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64/ - -gomobile bind -target=linux/amd64 -x -v -o build -tags=openssl mixin/kernel \ No newline at end of file diff --git a/library/go/build-windows.sh b/library/go/build-windows.sh deleted file mode 100755 index b412d11..0000000 --- a/library/go/build-windows.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# should install this version -#go install github.com/tougee/jvm/cmd/gomobile -#go install github.com/tougee/jvm/cmd/gobind - -# build on MacOS -# brew install mingw-w64 -# export CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ -# export JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home - -gomobile bind -target=windows/amd64 -x -v -o build -tags=openssl mixin/kernel \ No newline at end of file diff --git a/library/go/go.mod b/library/go/go.mod deleted file mode 100644 index 90ffebb..0000000 --- a/library/go/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module mixin - -go 1.21 - -toolchain go1.21.5 - -require ( - filippo.io/edwards25519 v1.0.0 - github.com/MixinNetwork/mixin v0.17.9 - github.com/stretchr/testify v1.8.4 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/klauspost/cpuid/v2 v2.2.6 // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/zeebo/blake3 v0.2.3 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/sys v0.15.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/library/go/go.sum b/library/go/go.sum deleted file mode 100644 index 1365eec..0000000 --- a/library/go/go.sum +++ /dev/null @@ -1,38 +0,0 @@ -filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= -filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -github.com/MixinNetwork/mixin v0.17.9 h1:OE5xIzaPeHpTHjqDFP+bKWyfbQEpn+C08QSVNoJOLqM= -github.com/MixinNetwork/mixin v0.17.9/go.mod h1:OPcspQJGtkh9GWFOp82Dal42FPzS3o2UX2nGYbnUeOU= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= -github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= -github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= -github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/library/go/kernel/address.go b/library/go/kernel/address.go deleted file mode 100644 index 248f51e..0000000 --- a/library/go/kernel/address.go +++ /dev/null @@ -1,32 +0,0 @@ -package kernel - -import "github.com/MixinNetwork/mixin/common" - -type Address struct { - address common.Address -} - -func NewMainAddressFromString(s string) (*Address, error) { - a, err := common.NewAddressFromString(s) - return &Address{a}, err -} - -func (a *Address) PublicSpendKey() []byte { - return a.address.PublicSpendKey[:] -} - -func (a *Address) PublicViewkey() []byte { - return a.address.PublicViewKey[:] -} - -func (a *Address) SetPublicSpendKey(k []byte) { - copy(a.address.PublicSpendKey[:], k[:]) -} - -func (a *Address) SetPublicViewKey(k []byte) { - copy(a.address.PublicViewKey[:], k[:]) -} - -func (a *Address) String() string { - return a.address.String() -} diff --git a/library/go/kernel/tx.go b/library/go/kernel/tx.go deleted file mode 100644 index 1c17f99..0000000 --- a/library/go/kernel/tx.go +++ /dev/null @@ -1,535 +0,0 @@ -package kernel - -import ( - "crypto/sha512" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "strings" - - "filippo.io/edwards25519" - "github.com/MixinNetwork/mixin/common" - "github.com/MixinNetwork/mixin/crypto" -) - -type Utxo struct { - Hash string `json:"hash"` - Index int `json:"index"` - Amount string `json:"amount"` -} - -type Tx struct { - Hash string `json:"hash"` - Raw string `json:"raw"` - Change *Utxo `json:"change,omitempty"` -} - -func BuildTxToKernelAddress(asset string, amount string, kenelAddress string, inputs []byte, changeKeys, changeMask, extra string) (string, error) { - a, err := common.NewAddressFromString(kenelAddress) - if err != nil { - return "", err - } - seed := make([]byte, 64) - crypto.ReadRand(seed) - r := crypto.NewKeyFromSeed(seed) - receiverMask := r.Public() - keys := crypto.DeriveGhostPublicKey(&r, &a.PublicViewKey, &a.PublicSpendKey, uint64(0)) - - ckeys := strings.Split(changeKeys, ",") - cks := []*crypto.Key{} - for _, k := range ckeys { - ke := k - rk, err := crypto.KeyFromString(ke) - if err != nil { - return "", err - } - cks = append(cks, &rk) - } - var utxo []Utxo - err = json.Unmarshal(inputs, &utxo) - if err != nil { - panic(err) - } - ins := []*common.UTXO{} - for _, u := range utxo { - ut := u - h, err := crypto.HashFromString(ut.Hash) - if err != nil { - return "", err - } - amount := common.NewIntegerFromString(ut.Amount) - u := common.UTXO{ - Input: common.Input{ - Hash: h, - Index: uint(ut.Index), - }, - Output: common.Output{ - Amount: amount, - }, - } - ins = append(ins, &u) - } - - return buildTransaction(asset, amount, 1, []*crypto.Key{keys}, receiverMask, ins, cks, changeMask, extra, "") -} - -func BuildTx(asset string, amount string, threshold int32, receiverKeys string, receiverMask string, inputs []byte, changeKeys, changeMask, extra, reference string) (string, error) { - keys := strings.Split(receiverKeys, ",") - rks := []*crypto.Key{} - for _, k := range keys { - key := k - rk, err := crypto.KeyFromString(key) - if err != nil { - return "", err - } - rks = append(rks, &rk) - } - ckeys := strings.Split(changeKeys, ",") - cks := []*crypto.Key{} - for _, k := range ckeys { - ke := k - rk, err := crypto.KeyFromString(ke) - if err != nil { - return "", err - } - cks = append(cks, &rk) - } - var utxo []Utxo - err := json.Unmarshal(inputs, &utxo) - if err != nil { - panic(err) - } - ins := []*common.UTXO{} - for _, u := range utxo { - ut := u - h, err := crypto.HashFromString(ut.Hash) - if err != nil { - return "", err - } - amount := common.NewIntegerFromString(ut.Amount) - u := common.UTXO{ - Input: common.Input{ - Hash: h, - Index: uint(ut.Index), - }, - Output: common.Output{ - Amount: amount, - }, - } - ins = append(ins, &u) - } - mask, err := crypto.KeyFromString(receiverMask) - if err != nil { - return "", err - } - return buildTransaction(asset, amount, threshold, rks, mask, ins, cks, changeMask, extra, reference) -} - -func BuildWithdrawalTx(asset string, amount, address, tag string, feeAmount, feeKeys string, feeMask string, inputs []byte, changeKeys, changeMask, extra string) (*Tx, error) { - rks := []*crypto.Key{} - if feeKeys != "" { - keys := strings.Split(feeKeys, ",") - for _, k := range keys { - key := k - rk, err := crypto.KeyFromString(key) - if err != nil { - return nil, err - } - rks = append(rks, &rk) - } - } - cks := []*crypto.Key{} - if changeKeys != "" { - ckeys := strings.Split(changeKeys, ",") - for _, k := range ckeys { - ke := k - rk, err := crypto.KeyFromString(ke) - if err != nil { - return nil, err - } - cks = append(cks, &rk) - } - } - var utxo []Utxo - err := json.Unmarshal(inputs, &utxo) - if err != nil { - panic(err) - } - ins := []*common.UTXO{} - for _, u := range utxo { - ut := u - h, err := crypto.HashFromString(ut.Hash) - if err != nil { - return nil, err - } - amount := common.NewIntegerFromString(ut.Amount) - u := common.UTXO{ - Input: common.Input{ - Hash: h, - Index: uint(ut.Index), - }, - Output: common.Output{ - Amount: amount, - }, - } - ins = append(ins, &u) - } - return buildWithrawalTransaction(asset, amount, ins, address, tag, feeAmount, rks, feeMask, cks, changeMask, extra) -} - -func buildWithrawalTransaction(asset, amount string, inputs []*common.UTXO, address, tag string, feeAmount string, feeKeys []*crypto.Key, feeMask string, changeKeys []*crypto.Key, changeMask string, extra string) (*Tx, error) { - assetHash, err := crypto.HashFromString(asset) - if err != nil { - return nil, err - } - - amountValue := common.NewIntegerFromString(amount) - feeAmountValue := common.NewInteger(0) - if feeAmount != "" { - feeAmountValue = common.NewIntegerFromString(feeAmount) - } - total := common.NewInteger(0) - - tx := common.NewTransactionV5(assetHash) - for _, in := range inputs { - tx.AddInput(in.Hash, in.Index) - total = total.Add(in.Amount) - } - if feeAmountValue.Cmp(common.Zero) > 0 && total.Cmp(amountValue.Add(feeAmountValue)) < 0 { - return nil, errors.New("insufficient funds") - } - withdrawalOutput := &common.Output{ - Type: common.OutputTypeWithdrawalSubmit, - Amount: amountValue, - Withdrawal: &common.WithdrawalData{ - Address: address, - Tag: tag, - }, - } - tx.Outputs = append(tx.Outputs, withdrawalOutput) - if feeAmount != "" { - if feeMask == "" { - return nil, errors.New("bad param address") - } - mask, err := crypto.KeyFromString(feeMask) - if err != nil { - return nil, err - } - if !mask.CheckKey() { - return nil, errors.New("invalid mask") - } - - feeOutput := &common.Output{ - Type: common.OutputTypeScript, - Amount: feeAmountValue, - Keys: feeKeys, - Mask: mask, - Script: common.NewThresholdScript(1), - } - tx.Outputs = append(tx.Outputs, feeOutput) - } - - amountAndFee := amountValue - if feeAmount != "" { - amountAndFee = amountAndFee.Add(feeAmountValue) - } - if total.Cmp(amountAndFee) > 0 { - change := total.Sub(amountAndFee) - script := common.NewThresholdScript(1) - - changeMaskKey, err := crypto.KeyFromString(changeMask) - if err != nil { - return nil, err - } - - out := &common.Output{ - Type: common.OutputTypeScript, - Amount: change, - Script: script, - Mask: changeMaskKey, - Keys: changeKeys, - } - tx.Outputs = append(tx.Outputs, out) - } - - if extra != "" { - extraBytes := []byte(extra) - if len(extraBytes) > 512 { - return nil, errors.New("extra data is too long") - } - tx.Extra = extraBytes - } - - ver := tx.AsVersioned() - t := &Tx{ - Raw: hex.EncodeToString(ver.Marshal()), - Hash: ver.PayloadHash().String(), - } - return t, nil -} - -func buildTransaction(asset string, amount string, threshold int32, receiverKeys []*crypto.Key, receiverMask crypto.Key, inputs []*common.UTXO, changeKeys []*crypto.Key, changeMask string, extra, reference string) (string, error) { - assetHash, err := crypto.HashFromString(asset) - if err != nil { - return "", err - } - - amountValue := common.NewIntegerFromString(amount) - total := common.NewInteger(0) - - tx := common.NewTransactionV5(assetHash) - for _, in := range inputs { - tx.AddInput(in.Hash, in.Index) - total = total.Add(in.Amount) - } - - if total.Cmp(amountValue) < 0 { - return "", errors.New("insufficient funds") - } - - if !receiverMask.CheckKey() { - return "", errors.New("invalid mask") - } - output := &common.Output{ - Type: common.OutputTypeScript, - Amount: amountValue, - Keys: receiverKeys, - Mask: receiverMask, - Script: common.NewThresholdScript(uint8(threshold)), - } - tx.Outputs = append(tx.Outputs, output) - - if reference != "" { - h, err := crypto.HashFromString(reference) - if err != nil { - return "", errors.New("bad param reference") - } - tx.References = append(tx.References, h) - } - if total.Cmp(amountValue) > 0 { - change := total.Sub(amountValue) - script := common.NewThresholdScript(1) - - changeMaskKey, err := crypto.KeyFromString(changeMask) - if err != nil { - return "", err - } - - out := &common.Output{ - Type: common.OutputTypeScript, - Amount: change, - Script: script, - Mask: changeMaskKey, - Keys: changeKeys, - } - tx.Outputs = append(tx.Outputs, out) - } - - if extra != "" { - extraBytes := []byte(extra) - if len(extraBytes) > 512 { - return "", errors.New("extra data is too long") - } - tx.Extra = extraBytes - } - - ver := tx.AsVersioned() - return hex.EncodeToString(ver.Marshal()), nil -} - -func SignTx(raw, inputKeys, viewKeys string, spendKey string, withoutFee bool) (*Tx, error) { - views := strings.Split(viewKeys, ",") - rawBytes, err := hex.DecodeString(raw) - if err != nil { - return nil, err - } - ver, err := common.UnmarshalVersionedTransaction(rawBytes) - if err != nil { - return nil, err - } - msg := ver.PayloadHash() - - var inputs [][]string - if err := json.Unmarshal([]byte(inputKeys), &inputs); err != nil { - return nil, err - } - - spendSeed, err := hex.DecodeString(spendKey) - if err != nil { - return nil, err - } - h := sha512.Sum512(spendSeed) - s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) - if err != nil { - return nil, err - } - y, err := edwards25519.NewScalar().SetCanonicalBytes(s.Bytes()) - if err != nil { - return nil, err - } - - for i, view := range views { - viewBytes, err := hex.DecodeString(view) - if err != nil { - return nil, err - } - x, err := edwards25519.NewScalar().SetCanonicalBytes(viewBytes) - if err != nil { - return nil, err - } - - t := edwards25519.NewScalar().Add(x, y) - var key crypto.Key - copy(key[:], t.Bytes()) - - input := inputs[i] - keysFilter := make(map[string]uint16) - for i, k := range input { - keysFilter[k] = uint16(i) - } - - i, found := keysFilter[key.Public().String()] - if !found { - return nil, fmt.Errorf("invalid public key for the input %s, %s", input, key.Public().String()) - } - sig := key.Sign(msg) - sigs := make(map[uint16]*crypto.Signature) - sigs[i] = &sig - ver.SignaturesMap = append(ver.SignaturesMap, sigs) - } - var changeUtxo *Utxo - if ver.Outputs[0].Withdrawal != nil { - if len(ver.Outputs) == 3 { - changeIndex := len(ver.Outputs) - 1 - changeUtxo = &Utxo{ - Hash: ver.PayloadHash().String(), - Amount: ver.Outputs[changeIndex].Amount.String(), - Index: changeIndex, - } - } else if len(ver.Outputs) == 2 { - if withoutFee { - changeIndex := len(ver.Outputs) - 1 - changeUtxo = &Utxo{ - Hash: ver.PayloadHash().String(), - Amount: ver.Outputs[changeIndex].Amount.String(), - Index: changeIndex, - } - } - } - } else { - if len(ver.Outputs) > 1 { - changeIndex := len(ver.Outputs) - 1 - changeUtxo = &Utxo{ - Hash: ver.PayloadHash().String(), - Amount: ver.Outputs[changeIndex].Amount.String(), - Index: changeIndex, - } - } - } - - transaction := &Tx{ - Hash: ver.PayloadHash().String(), - Raw: hex.EncodeToString(ver.Marshal()), - Change: changeUtxo, - } - return transaction, nil -} - -func SignTransaction(raw, viewKeys string, spendKey string, index uint16, withoutFee bool) (*Tx, error) { - views := strings.Split(viewKeys, ",") - rawBytes, err := hex.DecodeString(raw) - if err != nil { - return nil, err - } - ver, err := common.UnmarshalVersionedTransaction(rawBytes) - if err != nil { - return nil, err - } - msg := ver.PayloadHash() - spendSeed, err := hex.DecodeString(spendKey) - if err != nil { - return nil, err - } - h := sha512.Sum512(spendSeed) - s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) - if err != nil { - return nil, err - } - y, err := edwards25519.NewScalar().SetCanonicalBytes(s.Bytes()) - if err != nil { - return nil, err - } - - for _, view := range views { - viewBytes, err := hex.DecodeString(view) - if err != nil { - return nil, err - } - x, err := edwards25519.NewScalar().SetCanonicalBytes(viewBytes) - if err != nil { - return nil, err - } - - t := edwards25519.NewScalar().Add(x, y) - var key crypto.Key - copy(key[:], t.Bytes()) - - sig := key.Sign(msg) - sigs := make(map[uint16]*crypto.Signature) - sigs[index] = &sig - ver.SignaturesMap = append(ver.SignaturesMap, sigs) - } - var changeUtxo *Utxo - if ver.Outputs[0].Withdrawal != nil { - if len(ver.Outputs) == 3 { - changeIndex := len(ver.Outputs) - 1 - changeUtxo = &Utxo{ - Hash: ver.PayloadHash().String(), - Amount: ver.Outputs[changeIndex].Amount.String(), - Index: changeIndex, - } - } else if len(ver.Outputs) == 2 { - if withoutFee { - changeIndex := len(ver.Outputs) - 1 - changeUtxo = &Utxo{ - Hash: ver.PayloadHash().String(), - Amount: ver.Outputs[changeIndex].Amount.String(), - Index: changeIndex, - } - } - } - } else { - if len(ver.Outputs) > 1 { - changeIndex := len(ver.Outputs) - 1 - changeUtxo = &Utxo{ - Hash: ver.PayloadHash().String(), - Amount: ver.Outputs[changeIndex].Amount.String(), - Index: changeIndex, - } - } - } - - transaction := &Tx{ - Hash: ver.PayloadHash().String(), - Raw: hex.EncodeToString(ver.Marshal()), - Change: changeUtxo, - } - return transaction, nil -} - -func DecodeRawTx(raw string, _ int) (string, error) { - rawBytes, err := hex.DecodeString(raw) - if err != nil { - return "", err - } - ver, err := common.UnmarshalVersionedTransaction(rawBytes) - if err != nil { - return "", err - } - tx, err := json.Marshal(ver) - if err != nil { - return "", err - } - return string(tx), nil -} diff --git a/library/go/kernel/tx_test.go b/library/go/kernel/tx_test.go deleted file mode 100644 index e1ea7ac..0000000 --- a/library/go/kernel/tx_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package kernel - -import ( - "encoding/base64" - "encoding/json" - "log" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBuildWithdrawalTx(t *testing.T) { - assert := assert.New(t) - assetId := "5b9d576914e71e2362f89bb867eb69084931eb958f9a3622d776b861602275f4" - amount := "1" - address := "TV9mvdJv61mVtEoTY5h6kQtrvrULcFfadM" - tag := "" - feeAmount := "" - feeKeys := "" - feeMask := "" - inputs, err := base64.RawURLEncoding.DecodeString("W3siYW1vdW50IjoiMTAiLCJoYXNoIjoiZjY0MTA2OWU0Y2NjZTYyZTcwZGU1OTI1M2MxMGY2YzUzNzRlODdjMzYxYmMzMjhiODIwNzE5ZDc5MTRmYTJlZCIsImluZGV4IjowfV0") - assert.Nil(err) - changeKeys := "5a21338c6e0e731afec7b87fb52447e095fb47b602bb89b0eb6ee68e8252623a" - changeMask := "2c911e2b2cc4f847baadddee1bb4be927a3239a436c3e28f9e736f8d436f9311" - extra := "" - tx, err := BuildWithdrawalTx(assetId, amount, address, tag, feeAmount, feeKeys, feeMask, inputs, changeKeys, changeMask, extra) - assert.Nil(err) - assert.Equal("a6b10f4183f7e8ab358806ad08ed686cdc3b1983dd2612ea7df8f09b52d42bb3", tx.Hash) - d, err := json.Marshal(tx) - assert.Nil(err) - log.Println(string(d)) -} diff --git a/library/libs/darwin/amd64/libgojni.h b/library/libs/darwin/amd64/libgojni.h deleted file mode 100644 index bf2b3c6..0000000 --- a/library/libs/darwin/amd64/libgojni.h +++ /dev/null @@ -1,192 +0,0 @@ -/* Code generated by cmd/cgo; DO NOT EDIT. */ - -/* package gobind/gobind */ - - -#line 1 "cgo-builtin-export-prolog" - -#include - -#ifndef GO_CGO_EXPORT_PROLOGUE_H -#define GO_CGO_EXPORT_PROLOGUE_H - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef struct { const char *p; ptrdiff_t n; } _GoString_; -#endif - -#endif - -/* Start of preamble from import "C" comments. */ - - -#line 8 "go_kernelmain.go" - -#include -#include -#include "seq.h" -#include "kernel.h" - - -#line 1 "cgo-generated-wrapper" - -#line 8 "go_main.go" - -#include -#include -#include "seq.h" -#include "universe.h" - - -#line 1 "cgo-generated-wrapper" - -#line 11 "seq.go" - - - - - #include - #include "seq.h" - -#line 1 "cgo-generated-wrapper" - -#line 15 "seq_support.go" - - -#include -#include -#include -#include "seq_support.h" - -#line 1 "cgo-generated-wrapper" - - -/* End of preamble from import "C" comments. */ - - -/* Start of boilerplate cgo prologue. */ -#line 1 "cgo-gcc-export-header-prolog" - -#ifndef GO_CGO_PROLOGUE_H -#define GO_CGO_PROLOGUE_H - -typedef signed char GoInt8; -typedef unsigned char GoUint8; -typedef short GoInt16; -typedef unsigned short GoUint16; -typedef int GoInt32; -typedef unsigned int GoUint32; -typedef long long GoInt64; -typedef unsigned long long GoUint64; -typedef GoInt64 GoInt; -typedef GoUint64 GoUint; -typedef size_t GoUintptr; -typedef float GoFloat32; -typedef double GoFloat64; -#ifdef _MSC_VER -#include -typedef _Fcomplex GoComplex64; -typedef _Dcomplex GoComplex128; -#else -typedef float _Complex GoComplex64; -typedef double _Complex GoComplex128; -#endif - -/* - static assertion to make sure the file is being used on architecture - at least with matching size of GoInt. -*/ -typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef _GoString_ GoString; -#endif -typedef void *GoMap; -typedef void *GoChan; -typedef struct { void *t; void *v; } GoInterface; -typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; - -#endif - -/* End of boilerplate cgo prologue. */ - -#ifdef __cplusplus -extern "C" { -#endif - -extern nbyteslice proxykernel_Address_PublicSpendKey(int32_t refnum); -extern nbyteslice proxykernel_Address_PublicViewkey(int32_t refnum); -extern void proxykernel_Address_SetPublicSpendKey(int32_t refnum, nbyteslice param_k); -extern void proxykernel_Address_SetPublicViewKey(int32_t refnum, nbyteslice param_k); -extern nstring proxykernel_Address_String(int32_t refnum); -extern int32_t new_kernel_Address(); -extern void proxykernel_Tx_Hash_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Tx_Hash_Get(int32_t refnum); -extern void proxykernel_Tx_Raw_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Tx_Raw_Get(int32_t refnum); -extern void proxykernel_Tx_Change_Set(int32_t refnum, int32_t v); -extern int32_t proxykernel_Tx_Change_Get(int32_t refnum); -extern int32_t new_kernel_Tx(); -extern void proxykernel_Utxo_Hash_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Utxo_Hash_Get(int32_t refnum); -extern void proxykernel_Utxo_Index_Set(int32_t refnum, nint v); -extern nint proxykernel_Utxo_Index_Get(int32_t refnum); -extern void proxykernel_Utxo_Amount_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Utxo_Amount_Get(int32_t refnum); -extern int32_t new_kernel_Utxo(); - -/* Return type for proxykernel__BuildTx */ -struct proxykernel__BuildTx_return { - nstring r0; - int32_t r1; -}; -extern struct proxykernel__BuildTx_return proxykernel__BuildTx(nstring param_asset, nstring param_amount, int32_t param_threshold, nstring param_receiverKeys, nstring param_receiverMask, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra, nstring param_reference); - -/* Return type for proxykernel__BuildTxToKernelAddress */ -struct proxykernel__BuildTxToKernelAddress_return { - nstring r0; - int32_t r1; -}; -extern struct proxykernel__BuildTxToKernelAddress_return proxykernel__BuildTxToKernelAddress(nstring param_asset, nstring param_amount, nstring param_kenelAddress, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra); - -/* Return type for proxykernel__BuildWithdrawalTx */ -struct proxykernel__BuildWithdrawalTx_return { - int32_t r0; - int32_t r1; -}; -extern struct proxykernel__BuildWithdrawalTx_return proxykernel__BuildWithdrawalTx(nstring param_asset, nstring param_amount, nstring param_address, nstring param_tag, nstring param_feeAmount, nstring param_feeKeys, nstring param_feeMask, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra); - -/* Return type for proxykernel__DecodeRawTx */ -struct proxykernel__DecodeRawTx_return { - nstring r0; - int32_t r1; -}; -extern struct proxykernel__DecodeRawTx_return proxykernel__DecodeRawTx(nstring param_raw, nint param_p1); - -/* Return type for proxykernel__NewMainAddressFromString */ -struct proxykernel__NewMainAddressFromString_return { - int32_t r0; - int32_t r1; -}; -extern struct proxykernel__NewMainAddressFromString_return proxykernel__NewMainAddressFromString(nstring param_s); - -/* Return type for proxykernel__SignTx */ -struct proxykernel__SignTx_return { - int32_t r0; - int32_t r1; -}; - -// skipped function SignTransaction with unsupported parameter or result types -// -extern struct proxykernel__SignTx_return proxykernel__SignTx(nstring param_raw, nstring param_inputKeys, nstring param_viewKeys, nstring param_spendKey, char param_withoutFee); -extern nstring proxy_error_Error(int32_t refnum); - -// IncGoRef is called by foreign code to pin a Go object while its refnum is crossing -// the language barrier -extern void IncGoRef(int32_t refnum); - -// DestroyRef is called by Java to inform Go it is done with a reference. -extern void DestroyRef(int32_t refnum); - -#ifdef __cplusplus -} -#endif diff --git a/library/libs/darwin/amd64/libgojni.so b/library/libs/darwin/amd64/libgojni.so deleted file mode 100644 index 870ec61..0000000 Binary files a/library/libs/darwin/amd64/libgojni.so and /dev/null differ diff --git a/library/libs/kernel.jar b/library/libs/kernel.jar deleted file mode 100644 index 4633d09..0000000 Binary files a/library/libs/kernel.jar and /dev/null differ diff --git a/library/libs/linux/amd64/libgojni.h b/library/libs/linux/amd64/libgojni.h deleted file mode 100644 index bf2b3c6..0000000 --- a/library/libs/linux/amd64/libgojni.h +++ /dev/null @@ -1,192 +0,0 @@ -/* Code generated by cmd/cgo; DO NOT EDIT. */ - -/* package gobind/gobind */ - - -#line 1 "cgo-builtin-export-prolog" - -#include - -#ifndef GO_CGO_EXPORT_PROLOGUE_H -#define GO_CGO_EXPORT_PROLOGUE_H - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef struct { const char *p; ptrdiff_t n; } _GoString_; -#endif - -#endif - -/* Start of preamble from import "C" comments. */ - - -#line 8 "go_kernelmain.go" - -#include -#include -#include "seq.h" -#include "kernel.h" - - -#line 1 "cgo-generated-wrapper" - -#line 8 "go_main.go" - -#include -#include -#include "seq.h" -#include "universe.h" - - -#line 1 "cgo-generated-wrapper" - -#line 11 "seq.go" - - - - - #include - #include "seq.h" - -#line 1 "cgo-generated-wrapper" - -#line 15 "seq_support.go" - - -#include -#include -#include -#include "seq_support.h" - -#line 1 "cgo-generated-wrapper" - - -/* End of preamble from import "C" comments. */ - - -/* Start of boilerplate cgo prologue. */ -#line 1 "cgo-gcc-export-header-prolog" - -#ifndef GO_CGO_PROLOGUE_H -#define GO_CGO_PROLOGUE_H - -typedef signed char GoInt8; -typedef unsigned char GoUint8; -typedef short GoInt16; -typedef unsigned short GoUint16; -typedef int GoInt32; -typedef unsigned int GoUint32; -typedef long long GoInt64; -typedef unsigned long long GoUint64; -typedef GoInt64 GoInt; -typedef GoUint64 GoUint; -typedef size_t GoUintptr; -typedef float GoFloat32; -typedef double GoFloat64; -#ifdef _MSC_VER -#include -typedef _Fcomplex GoComplex64; -typedef _Dcomplex GoComplex128; -#else -typedef float _Complex GoComplex64; -typedef double _Complex GoComplex128; -#endif - -/* - static assertion to make sure the file is being used on architecture - at least with matching size of GoInt. -*/ -typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef _GoString_ GoString; -#endif -typedef void *GoMap; -typedef void *GoChan; -typedef struct { void *t; void *v; } GoInterface; -typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; - -#endif - -/* End of boilerplate cgo prologue. */ - -#ifdef __cplusplus -extern "C" { -#endif - -extern nbyteslice proxykernel_Address_PublicSpendKey(int32_t refnum); -extern nbyteslice proxykernel_Address_PublicViewkey(int32_t refnum); -extern void proxykernel_Address_SetPublicSpendKey(int32_t refnum, nbyteslice param_k); -extern void proxykernel_Address_SetPublicViewKey(int32_t refnum, nbyteslice param_k); -extern nstring proxykernel_Address_String(int32_t refnum); -extern int32_t new_kernel_Address(); -extern void proxykernel_Tx_Hash_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Tx_Hash_Get(int32_t refnum); -extern void proxykernel_Tx_Raw_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Tx_Raw_Get(int32_t refnum); -extern void proxykernel_Tx_Change_Set(int32_t refnum, int32_t v); -extern int32_t proxykernel_Tx_Change_Get(int32_t refnum); -extern int32_t new_kernel_Tx(); -extern void proxykernel_Utxo_Hash_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Utxo_Hash_Get(int32_t refnum); -extern void proxykernel_Utxo_Index_Set(int32_t refnum, nint v); -extern nint proxykernel_Utxo_Index_Get(int32_t refnum); -extern void proxykernel_Utxo_Amount_Set(int32_t refnum, nstring v); -extern nstring proxykernel_Utxo_Amount_Get(int32_t refnum); -extern int32_t new_kernel_Utxo(); - -/* Return type for proxykernel__BuildTx */ -struct proxykernel__BuildTx_return { - nstring r0; - int32_t r1; -}; -extern struct proxykernel__BuildTx_return proxykernel__BuildTx(nstring param_asset, nstring param_amount, int32_t param_threshold, nstring param_receiverKeys, nstring param_receiverMask, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra, nstring param_reference); - -/* Return type for proxykernel__BuildTxToKernelAddress */ -struct proxykernel__BuildTxToKernelAddress_return { - nstring r0; - int32_t r1; -}; -extern struct proxykernel__BuildTxToKernelAddress_return proxykernel__BuildTxToKernelAddress(nstring param_asset, nstring param_amount, nstring param_kenelAddress, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra); - -/* Return type for proxykernel__BuildWithdrawalTx */ -struct proxykernel__BuildWithdrawalTx_return { - int32_t r0; - int32_t r1; -}; -extern struct proxykernel__BuildWithdrawalTx_return proxykernel__BuildWithdrawalTx(nstring param_asset, nstring param_amount, nstring param_address, nstring param_tag, nstring param_feeAmount, nstring param_feeKeys, nstring param_feeMask, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra); - -/* Return type for proxykernel__DecodeRawTx */ -struct proxykernel__DecodeRawTx_return { - nstring r0; - int32_t r1; -}; -extern struct proxykernel__DecodeRawTx_return proxykernel__DecodeRawTx(nstring param_raw, nint param_p1); - -/* Return type for proxykernel__NewMainAddressFromString */ -struct proxykernel__NewMainAddressFromString_return { - int32_t r0; - int32_t r1; -}; -extern struct proxykernel__NewMainAddressFromString_return proxykernel__NewMainAddressFromString(nstring param_s); - -/* Return type for proxykernel__SignTx */ -struct proxykernel__SignTx_return { - int32_t r0; - int32_t r1; -}; - -// skipped function SignTransaction with unsupported parameter or result types -// -extern struct proxykernel__SignTx_return proxykernel__SignTx(nstring param_raw, nstring param_inputKeys, nstring param_viewKeys, nstring param_spendKey, char param_withoutFee); -extern nstring proxy_error_Error(int32_t refnum); - -// IncGoRef is called by foreign code to pin a Go object while its refnum is crossing -// the language barrier -extern void IncGoRef(int32_t refnum); - -// DestroyRef is called by Java to inform Go it is done with a reference. -extern void DestroyRef(int32_t refnum); - -#ifdef __cplusplus -} -#endif diff --git a/library/libs/linux/amd64/libgojni.so b/library/libs/linux/amd64/libgojni.so deleted file mode 100644 index dc1fb2a..0000000 Binary files a/library/libs/linux/amd64/libgojni.so and /dev/null differ diff --git a/library/libs/windows/amd64/libgojni.dll b/library/libs/windows/amd64/libgojni.dll deleted file mode 100644 index 19d737f..0000000 Binary files a/library/libs/windows/amd64/libgojni.dll and /dev/null differ diff --git a/library/libs/windows/amd64/libgojni.h b/library/libs/windows/amd64/libgojni.h deleted file mode 100644 index 23e3e52..0000000 --- a/library/libs/windows/amd64/libgojni.h +++ /dev/null @@ -1,192 +0,0 @@ -/* Code generated by cmd/cgo; DO NOT EDIT. */ - -/* package gobind/gobind */ - - -#line 1 "cgo-builtin-export-prolog" - -#include - -#ifndef GO_CGO_EXPORT_PROLOGUE_H -#define GO_CGO_EXPORT_PROLOGUE_H - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef struct { const char *p; ptrdiff_t n; } _GoString_; -#endif - -#endif - -/* Start of preamble from import "C" comments. */ - - -#line 8 "go_kernelmain.go" - -#include -#include -#include "seq.h" -#include "kernel.h" - - -#line 1 "cgo-generated-wrapper" - -#line 8 "go_main.go" - -#include -#include -#include "seq.h" -#include "universe.h" - - -#line 1 "cgo-generated-wrapper" - -#line 11 "seq.go" - - - - - #include - #include "seq.h" - -#line 1 "cgo-generated-wrapper" - -#line 15 "seq_support.go" - - -#include -#include -#include -#include "seq_support.h" - -#line 1 "cgo-generated-wrapper" - - -/* End of preamble from import "C" comments. */ - - -/* Start of boilerplate cgo prologue. */ -#line 1 "cgo-gcc-export-header-prolog" - -#ifndef GO_CGO_PROLOGUE_H -#define GO_CGO_PROLOGUE_H - -typedef signed char GoInt8; -typedef unsigned char GoUint8; -typedef short GoInt16; -typedef unsigned short GoUint16; -typedef int GoInt32; -typedef unsigned int GoUint32; -typedef long long GoInt64; -typedef unsigned long long GoUint64; -typedef GoInt64 GoInt; -typedef GoUint64 GoUint; -typedef size_t GoUintptr; -typedef float GoFloat32; -typedef double GoFloat64; -#ifdef _MSC_VER -#include -typedef _Fcomplex GoComplex64; -typedef _Dcomplex GoComplex128; -#else -typedef float _Complex GoComplex64; -typedef double _Complex GoComplex128; -#endif - -/* - static assertion to make sure the file is being used on architecture - at least with matching size of GoInt. -*/ -typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; - -#ifndef GO_CGO_GOSTRING_TYPEDEF -typedef _GoString_ GoString; -#endif -typedef void *GoMap; -typedef void *GoChan; -typedef struct { void *t; void *v; } GoInterface; -typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; - -#endif - -/* End of boilerplate cgo prologue. */ - -#ifdef __cplusplus -extern "C" { -#endif - -extern __declspec(dllexport) nbyteslice proxykernel_Address_PublicSpendKey(int32_t refnum); -extern __declspec(dllexport) nbyteslice proxykernel_Address_PublicViewkey(int32_t refnum); -extern __declspec(dllexport) void proxykernel_Address_SetPublicSpendKey(int32_t refnum, nbyteslice param_k); -extern __declspec(dllexport) void proxykernel_Address_SetPublicViewKey(int32_t refnum, nbyteslice param_k); -extern __declspec(dllexport) nstring proxykernel_Address_String(int32_t refnum); -extern __declspec(dllexport) int32_t new_kernel_Address(); -extern __declspec(dllexport) void proxykernel_Tx_Hash_Set(int32_t refnum, nstring v); -extern __declspec(dllexport) nstring proxykernel_Tx_Hash_Get(int32_t refnum); -extern __declspec(dllexport) void proxykernel_Tx_Raw_Set(int32_t refnum, nstring v); -extern __declspec(dllexport) nstring proxykernel_Tx_Raw_Get(int32_t refnum); -extern __declspec(dllexport) void proxykernel_Tx_Change_Set(int32_t refnum, int32_t v); -extern __declspec(dllexport) int32_t proxykernel_Tx_Change_Get(int32_t refnum); -extern __declspec(dllexport) int32_t new_kernel_Tx(); -extern __declspec(dllexport) void proxykernel_Utxo_Hash_Set(int32_t refnum, nstring v); -extern __declspec(dllexport) nstring proxykernel_Utxo_Hash_Get(int32_t refnum); -extern __declspec(dllexport) void proxykernel_Utxo_Index_Set(int32_t refnum, nint v); -extern __declspec(dllexport) nint proxykernel_Utxo_Index_Get(int32_t refnum); -extern __declspec(dllexport) void proxykernel_Utxo_Amount_Set(int32_t refnum, nstring v); -extern __declspec(dllexport) nstring proxykernel_Utxo_Amount_Get(int32_t refnum); -extern __declspec(dllexport) int32_t new_kernel_Utxo(); - -/* Return type for proxykernel__BuildTx */ -struct proxykernel__BuildTx_return { - nstring r0; - int32_t r1; -}; -extern __declspec(dllexport) struct proxykernel__BuildTx_return proxykernel__BuildTx(nstring param_asset, nstring param_amount, int32_t param_threshold, nstring param_receiverKeys, nstring param_receiverMask, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra, nstring param_reference); - -/* Return type for proxykernel__BuildTxToKernelAddress */ -struct proxykernel__BuildTxToKernelAddress_return { - nstring r0; - int32_t r1; -}; -extern __declspec(dllexport) struct proxykernel__BuildTxToKernelAddress_return proxykernel__BuildTxToKernelAddress(nstring param_asset, nstring param_amount, nstring param_kenelAddress, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra); - -/* Return type for proxykernel__BuildWithdrawalTx */ -struct proxykernel__BuildWithdrawalTx_return { - int32_t r0; - int32_t r1; -}; -extern __declspec(dllexport) struct proxykernel__BuildWithdrawalTx_return proxykernel__BuildWithdrawalTx(nstring param_asset, nstring param_amount, nstring param_address, nstring param_tag, nstring param_feeAmount, nstring param_feeKeys, nstring param_feeMask, nbyteslice param_inputs, nstring param_changeKeys, nstring param_changeMask, nstring param_extra); - -/* Return type for proxykernel__DecodeRawTx */ -struct proxykernel__DecodeRawTx_return { - nstring r0; - int32_t r1; -}; -extern __declspec(dllexport) struct proxykernel__DecodeRawTx_return proxykernel__DecodeRawTx(nstring param_raw, nint param_p1); - -/* Return type for proxykernel__NewMainAddressFromString */ -struct proxykernel__NewMainAddressFromString_return { - int32_t r0; - int32_t r1; -}; -extern __declspec(dllexport) struct proxykernel__NewMainAddressFromString_return proxykernel__NewMainAddressFromString(nstring param_s); - -/* Return type for proxykernel__SignTx */ -struct proxykernel__SignTx_return { - int32_t r0; - int32_t r1; -}; - -// skipped function SignTransaction with unsupported parameter or result types -// -extern __declspec(dllexport) struct proxykernel__SignTx_return proxykernel__SignTx(nstring param_raw, nstring param_inputKeys, nstring param_viewKeys, nstring param_spendKey, char param_withoutFee); -extern __declspec(dllexport) nstring proxy_error_Error(int32_t refnum); - -// IncGoRef is called by foreign code to pin a Go object while its refnum is crossing -// the language barrier -extern __declspec(dllexport) void IncGoRef(int32_t refnum); - -// DestroyRef is called by Java to inform Go it is done with a reference. -extern __declspec(dllexport) void DestroyRef(int32_t refnum); - -#ifdef __cplusplus -} -#endif diff --git a/library/src/main/kotlin/one/mixin/bot/Constants.kt b/library/src/main/kotlin/one/mixin/bot/Constants.kt index 4605f7e..2e1244a 100644 --- a/library/src/main/kotlin/one/mixin/bot/Constants.kt +++ b/library/src/main/kotlin/one/mixin/bot/Constants.kt @@ -11,4 +11,6 @@ object Constants { } const val LIMIT = 30 + + const val MIXIN_FEE_USER_ID = "674d6776-d600-4346-af46-58e77d8df185" } diff --git a/library/src/main/kotlin/one/mixin/bot/HttpClient.kt b/library/src/main/kotlin/one/mixin/bot/HttpClient.kt index 10db356..dab9877 100644 --- a/library/src/main/kotlin/one/mixin/bot/HttpClient.kt +++ b/library/src/main/kotlin/one/mixin/bot/HttpClient.kt @@ -79,6 +79,10 @@ class HttpClient private constructor( retrofit.create(UtxoService::class.java) } + val tokenService: TokenService by lazy { + retrofit.create(TokenService::class.java) + } + val externalService: ExternalService by lazy { object : ExternalService { override fun getUtxoCall(hash: String, index: Int): Call { diff --git a/library/src/main/kotlin/one/mixin/bot/api/TokenService.kt b/library/src/main/kotlin/one/mixin/bot/api/TokenService.kt index 6d3ebc0..f1f13a1 100644 --- a/library/src/main/kotlin/one/mixin/bot/api/TokenService.kt +++ b/library/src/main/kotlin/one/mixin/bot/api/TokenService.kt @@ -1,6 +1,6 @@ package one.mixin.bot.api -import one.mixin.bot.api.call.UserCallService -import one.mixin.bot.api.coroutine.UserCoroutineService +import one.mixin.bot.api.call.TokenCallService +import one.mixin.bot.api.coroutine.TokenCoroutineService -interface TokenService : UserCallService, UserCoroutineService +interface TokenService : TokenCallService, TokenCoroutineService diff --git a/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt b/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt index b9080e4..0bbd49e 100644 --- a/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt +++ b/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt @@ -1,105 +1,324 @@ package one.mixin.bot.safe -import kernel.Kernel +import kotlinx.coroutines.runBlocking +import one.mixin.bot.Constants import one.mixin.bot.HttpClient -import one.mixin.bot.extension.assetIdToAsset -import one.mixin.bot.extension.isUUID +import one.mixin.bot.api.MixinResponse +import one.mixin.bot.extension.hexStringToByteArray import one.mixin.bot.extension.toHex +import one.mixin.bot.safe.tx.Transaction +import one.mixin.bot.util.blake3 import one.mixin.bot.util.sha3Sum256 -import one.mixin.bot.vo.buildGhostKeyRequest -import one.mixin.bot.vo.safe.MixAddress +import one.mixin.bot.util.uniqueObjectId +import one.mixin.bot.vo.GhostKeyRequest import one.mixin.bot.vo.safe.Output -import one.mixin.bot.vo.safe.SignResult -import one.mixin.bot.vo.safe.TransactionRecipient import one.mixin.bot.vo.safe.TransactionRequest import one.mixin.bot.vo.safe.TransactionResponse -import one.mixin.bot.vo.safe.UtxoWrapper import java.io.IOException import java.math.BigDecimal -import kotlin.jvm.Throws +import java.util.UUID +import one.mixin.bot.safe.tx.TransactionRecipient as TxRecipient -@Throws(SafeException::class, IOException::class) -fun sendTransaction( + +private fun verifyTxId(botClient: HttpClient, traceId: String) { + val txIdResp = botClient.utxoService.getTransactionsByIdCall(traceId).execute().body() + ?: throw SafeException("get safe/transactions/{id} got null response") + if (txIdResp.error?.code != 404) { + throw SafeException("get safe/transactions/{id} data: ${txIdResp.data}, error: ${txIdResp.error}") + } +} + +fun sendTransactionToUser( botClient: HttpClient, assetId: String, - recipient: TransactionRecipient, - traceId: String, + receivers: List, + amount: String, memo: String?, + traceId: String, ): List { - // verify trace id may have been signed already - val txIdResp = - botClient.utxoService.getTransactionsByIdCall(traceId).execute().body() - ?: throw SafeException("get safe/transactions/{id} got null response") - if (txIdResp.error?.code != 404) { - throw SafeException("get safe/transactions/{id} data: ${txIdResp.data}, error: ${txIdResp.error}") - } + verifyTxId(botClient, traceId) // check assetId is kernel assetId - // check assetId is kernel assetId - val asset = - if (assetId.isUUID()) { - assetIdToAsset(assetId) - } else { - assetId + // get unspent outputs for asset and may throw insufficient outputs error + val (utxos, changeAmount) = requestUnspentOutputsForRecipients(botClient, assetId, amount) + + val recipients = buildList { + add(TxRecipient.User(receivers, amount, 1)) + if (changeAmount > BigDecimal.ZERO) { + add( + TxRecipient.User( + utxos.first().receivers, changeAmount.toString(), utxos.first().receiversThreshold + ) + ) } - // get unspent outputs for asset and may throw insufficient outputs error - val (utxos, changeAmount) = requestUnspentOutputsForRecipients(botClient, assetId, recipient) - - // change to the sender - if (changeAmount > BigDecimal.ZERO) { - val ma = - MixAddress.newUuidMixAddress(listOf(botClient.safeUser.userId), 1) - ?: throw SafeException("newUuidMixAddress got null mixAddress") - val tr = TransactionRecipient(ma, changeAmount.toString()) - // TODO } - // request ghost key - val ghostKeyReq = buildGhostKeyRequest(recipient.mixAddress.uuidMembers.sorted(), listOf(botClient.safeUser.userId), traceId) - val ghostKeyResp = botClient.utxoService.ghostKeyCall(ghostKeyReq).execute().body() - if (ghostKeyResp == null || !ghostKeyResp.isSuccess()) { - throw SafeException("request ghostKey ${ghostKeyResp?.error}") + val ghostKeyRequest = buildList { + val output = uniqueObjectId(traceId, "OUTPUT", "0") + add(GhostKeyRequest(receivers, 0, output)) + if (changeAmount > BigDecimal.ZERO) { + val change = uniqueObjectId(traceId, "OUTPUT", "1") + add(GhostKeyRequest(utxos.first().receivers, 1, change)) + } + } + val ghostKeyResponse = botClient.utxoService.ghostKeyCall(ghostKeyRequest).execute().body() + + if (ghostKeyResponse == null || !ghostKeyResponse.isSuccess()) { + throw SafeException("request ghostKey ${ghostKeyResponse?.error}") } - val ghostKeys = ghostKeyResp.data ?: throw SafeException("ghost key response data null") - - // build the unsigned raw transaction - val utxoWrapper = UtxoWrapper(utxos) - val receiverKeys = ghostKeys.first().keys.joinToString(",") - val receiverMask = ghostKeys.first().mask - val changeKeys = ghostKeys.last().keys.joinToString(",") - val changeMask = ghostKeys.last().mask - val tx = Kernel.buildTx(asset, recipient.amount, recipient.mixAddress.threshold.toInt(), receiverKeys, receiverMask, utxoWrapper.input, changeKeys, changeMask, memo, "") - var txResp = botClient.utxoService.transactionRequestCall(listOf(TransactionRequest(tx, traceId))).execute().body() - if (txResp == null || !txResp.isSuccess()) { - throw SafeException("request transaction ${txResp?.error}") + val ghostKeys = ghostKeyResponse.data ?: throw SafeException("ghost key response data null") + + val tx = Transaction.build( + utxos = utxos, + recipients = recipients, + ghostsKeys = ghostKeys, + extra = memo ?: "", + ) + + val verifiedResp = botClient.utxoService.transactionRequestCall( + listOf(TransactionRequest(tx.encodeToString(), traceId)) + ).execute().body() + if (verifiedResp == null || !verifiedResp.isSuccess()) { + throw SafeException("request transaction ${verifiedResp?.error}") } - val txData = txResp.data ?: throw SafeException("request transaction response data null") + val verifiedTx = verifiedResp.data?.firstOrNull() ?: throw SafeException("request transaction response data null") - // sign transaction val spendKey = botClient.safeUser.spendPrivateKey ?: throw SafeException("spend key is null") - val views = txData.first().views.joinToString(",") - val keys = utxoWrapper.formatKeys - val sign = Kernel.signTx(tx, keys, views, spendKey.toHex(), false) - val signResult = SignResult(sign.raw, sign.change) - - txResp = botClient.utxoService.transactionsCall(listOf(TransactionRequest(signResult.raw, traceId))).execute().body() - if (txResp == null || !txResp.isSuccess()) { - throw SafeException("safe/transactions ${txResp?.error}") + val signedRaw = tx.sign(verifiedTx.views, utxos, spendKey.toHex()) + + val sendTx = botClient.utxoService.transactionsCall( + listOf(TransactionRequest(signedRaw, traceId)) + ).execute().body() + + if (sendTx == null || !sendTx.isSuccess()) { + throw SafeException("safe/transactions ${sendTx?.error}") + } + return sendTx.data!! +} + + +@JvmName("withdrawalToAddress") +@Throws(SafeException::class, IOException::class, UtxoException::class) +fun withdrawalToAddressBlocking( + botClient: HttpClient, + assetId: String, + destination: String, + tag: String?, + amount: String, + memo: String? = null, + traceId: String = UUID.randomUUID().toString(), +): List = runBlocking { + withdrawalToAddress(botClient, assetId, destination, tag, amount, memo, traceId) +} + +@JvmSynthetic +@Throws(SafeException::class, IOException::class, UtxoException::class) +suspend fun withdrawalToAddress( + botClient: HttpClient, + assetId: String, + destination: String, + tag: String?, + amount: String, + memo: String? = null, + traceId: String = UUID.randomUUID().toString(), +): List { + verifyTxId(botClient, traceId) + val token = botClient.tokenService.getAssetById(assetId).requiredData() + val chain = if (token.assetId == token.chainId) { + token + } else { + botClient.tokenService.getAssetById(token.chainId).requiredData() + } + + val fee = botClient.tokenService.getFees(assetId, destination).requiredData().first { + it.assetId == chain.assetId + } + + return botClient.withdrawalTransaction( + feeReceiverId = Constants.MIXIN_FEE_USER_ID, + feeAssetId = chain.assetId, + feeAmount = fee.amount!!.toBigDecimal(), + assetId = token.assetId, + amount = amount.toBigDecimal(), + destination = destination, + tag = tag, + memo = memo, + traceId = traceId, + ) + +} + + +private suspend fun HttpClient.withdrawalTransaction( + feeReceiverId: String, + feeAssetId: String, + feeAmount: BigDecimal, + assetId: String, + amount: BigDecimal, + destination: String, + tag: String?, + memo: String?, + traceId: String, +): List = if (feeAssetId != assetId) { + val (utxos, change) = requestUnspentOutputsForRecipients(this, assetId, amount.toPlainString()) + val (feeUtxos, feeChange) = requestUnspentOutputsForRecipients(this, feeAssetId, feeAmount.toPlainString()) + + val feeTraceId = uniqueObjectId(traceId, "FEE") + + val ghosts = utxoService.ghostKey(buildList { // fee + add(GhostKeyRequest(listOf(feeReceiverId), 0, uniqueObjectId(feeTraceId, "OUTPUT", "0"))) + + // change + if (change > BigDecimal.ZERO) { + add(GhostKeyRequest(utxos.first().receivers, 1, uniqueObjectId(traceId, "OUTPUT", "1"))) + } + + // fee change + if (feeChange > BigDecimal.ZERO) { + add(GhostKeyRequest(feeUtxos.first().receivers, 1, uniqueObjectId(feeTraceId, "OUTPUT", "1"))) + } + }).requiredData() + + val feeGhostKey = ghosts.first() + val changeGhostKey = if (change > BigDecimal.ZERO) ghosts[1] else null + val feeChangeGhostKey = if (feeChange > BigDecimal.ZERO) ghosts.last() else null + + val withdrawalTx = Transaction.build( + utxos = utxos, + recipients = buildList { + add(TxRecipient.Withdrawal(destination = destination, tag = tag, amount = amount.toPlainString())) + if (change > BigDecimal.ZERO) { + add( + TxRecipient.User( + members = utxos.first().receivers, + amount = change.toPlainString(), + threshold = utxos.first().receiversThreshold, + ) + ) + } + + }, + ghostsKeys = buildList { + add(null) // first is for withdrawal + if (change > BigDecimal.ZERO) { + add(changeGhostKey) + } + }, + extra = memo ?: "", + ) + val feeTx = Transaction.build( + utxos = feeUtxos, + recipients = buildList { + add(TxRecipient.User(listOf(feeReceiverId), feeAmount.toPlainString(), 1)) + if (feeChange > BigDecimal.ZERO) { + add( + TxRecipient.User( + members = feeUtxos.first().receivers, + amount = feeChange.toPlainString(), + threshold = feeUtxos.first().receiversThreshold, + ) + ) + } + }, + ghostsKeys = buildList { + add(feeGhostKey) + if (feeChange > BigDecimal.ZERO) { + add(feeChangeGhostKey) + } + }, + extra = memo ?: "", + reference = withdrawalTx.encodeToString().hexStringToByteArray().blake3().toHex(), + ) + + val requestResponse = utxoService.transactionRequest( + listOf( + TransactionRequest(withdrawalTx.encodeToString(), traceId), + TransactionRequest(feeTx.encodeToString(), feeTraceId), + ) + ).requiredData() + if (requestResponse.isEmpty()) { + throw SafeException("request transaction response data null") + } else if (requestResponse.first().state != "unspent") { + throw SafeException("request transaction state not unspent") } - return txResp.data as List + + val withdrawalData = requestResponse.first { it.requestId == traceId } + val feeData = requestResponse.first { it.requestId == feeTraceId } + + val spendKey = safeUser.spendPrivateKey ?: throw SafeException("spend key is null") + + val signedWithdrawalRaw = withdrawalTx.sign(withdrawalData.views, utxos, spendKey.toHex()) + val signedFeeRaw = feeTx.sign(feeData.views, feeUtxos, spendKey.toHex()) + + utxoService.transactions( + listOf( + TransactionRequest(signedWithdrawalRaw, traceId), + TransactionRequest(signedFeeRaw, feeTraceId), + ) + ).requiredData() + +} else { + val (utxos, changeAmount) = requestUnspentOutputsForRecipients(this, assetId, (amount + feeAmount).toPlainString()) + + val ghostKeys = utxoService.ghostKey(buildList { // fee + + // fee + val output = uniqueObjectId(traceId, "OUTPUT", "1") + add(GhostKeyRequest(listOf(feeReceiverId), 1, output)) + + // change + if (changeAmount > BigDecimal.ZERO) { + val change = uniqueObjectId(traceId, "OUTPUT", "2") + add(GhostKeyRequest(utxos.first().receivers, 2, change)) + } + }).requiredData() + + val tx = Transaction.build( + utxos = utxos, + recipients = buildList { + + // withdrawal + add(TxRecipient.Withdrawal(destination = destination, tag = tag, amount = amount.toPlainString())) + + // fee + add(TxRecipient.User(members = listOf(feeReceiverId), amount = feeAmount.toPlainString(), threshold = 1)) + + // change + if (changeAmount > BigDecimal.ZERO) { + add( + TxRecipient.User( + members = utxos.first().receivers, + amount = changeAmount.toPlainString(), + threshold = utxos.first().receiversThreshold + ) + ) + } + }, + ghostsKeys = listOf(null) /* first is for withdrawal */ + ghostKeys, + extra = memo ?: "", + ) + + val verifiedTx = + utxoService.transactionRequest(listOf(TransactionRequest(tx.encodeToString(), traceId))).requiredData() + .firstOrNull() ?: throw SafeException("request transaction response data null") + + val spendKey = safeUser.spendPrivateKey ?: throw SafeException("spend key is null") + val signedRaw = tx.sign(verifiedTx.views, utxos, spendKey.toHex()) + + utxoService.transactions(listOf(TransactionRequest(signedRaw, traceId))).requiredData() } -fun requestUnspentOutputsForRecipients( +private fun requestUnspentOutputsForRecipients( botClient: HttpClient, assetId: String, - recipient: TransactionRecipient, + amount: String, ): Pair, BigDecimal> { val memberHash = buildHashMembers(listOf(botClient.safeUser.userId)) val outputs = listUnspentOutputs(botClient, memberHash, 1, assetId) if (outputs.isEmpty()) { throw UtxoException(BigDecimal.ZERO, BigDecimal.ZERO, 0) } - val totalOutput = BigDecimal(recipient.amount) + val totalOutput = BigDecimal(amount) val selectedOutputs = mutableListOf() var totalInput = BigDecimal.ZERO outputs.forEach { o -> @@ -114,8 +333,12 @@ fun requestUnspentOutputsForRecipients( } fun buildHashMembers(ids: List): String { - return ids.sortedBy { it } - .joinToString("") - .sha3Sum256() - .joinToString("") { "%02x".format(it) } + return ids.sortedBy { it }.joinToString("").sha3Sum256().joinToString("") { "%02x".format(it) } +} + +private fun MixinResponse.requiredData(): T { + if (isSuccess()) { + return data!! + } + throw SafeException("response error: $error") } diff --git a/library/src/main/kotlin/one/mixin/bot/safe/tx/Encoder.kt b/library/src/main/kotlin/one/mixin/bot/safe/tx/Encoder.kt new file mode 100644 index 0000000..a5bc39b --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/safe/tx/Encoder.kt @@ -0,0 +1,164 @@ +package one.mixin.bot.safe.tx + +import com.ionspin.kotlin.bignum.integer.BigInteger +import okio.Buffer +import one.mixin.bot.extension.hexStringToByteArray +import one.mixin.bot.extension.toHex +import one.mixin.bot.util.toLeByteArray + +private val magic = ByteArray(2) { 0x77 } +private val empty = ByteArray(2) { 0x00 } + +private const val ExtraSizeStorageCapacity = 1024 * 1024 * 4 + +class Encoder { + + private val buffer = Buffer() + + private fun write(array: ByteArray) { + buffer.write(array) + } + + fun writeUint16(value: Int) { + buffer.writeShort(value) + } + + private fun writeUint32(value: Int) { + buffer.writeInt(value) + } + + private fun writeUint64(value: ULong) { + buffer.writeLong(value.toLong()) + } + + private fun writeBigInteger(value: BigInteger) { + val bytes = value.toByteArray() + writeUint16(bytes.size) + write(bytes) + } + + fun encodeTransaction(tx: Transaction) { + write(magic) + write(byteArrayOf(0x00, tx.version)) + + write(tx.asset.hexStringToByteArray()) + + writeUint16(tx.inputs.size) + for (input in tx.inputs) { + encodeInput(input) + } + + writeUint16(tx.outputs.size) + for (output in tx.outputs) { + encodeOutput(output) + } + + writeUint16(tx.reference.size) + for (reference in tx.reference) { + write(reference.hexStringToByteArray()) + } + + val extra = tx.extra.toByteArray() + require(extra.size <= ExtraSizeStorageCapacity) { "extra is too long" } + writeUint32(extra.size) + write(extra) + + } + + private fun encodeInput(i: Input) { + require(i.index <= 1024) { "index is too large" } + + write(i.hash.hexStringToByteArray()) + writeUint16(i.index) + + val genesis = i.genesis?.toByteArray() ?: ByteArray(0) + writeUint16(genesis.size) + write(genesis) + + if (i.deposit == null) { + write(empty) + } else { + val d = i.deposit + + write(magic) + write(d.chain.hexStringToByteArray()) + + d.assetKey.toByteArray().let { + writeUint16(it.size) + write(it) + } + + d.transaction.toByteArray().let { + writeUint16(it.size) + write(it) + } + + writeUint64(d.index) + writeBigInteger(d.amount) + + } + + if (i.mint == null) { + write(empty) + } else { + write(magic) + + i.mint.group.toByteArray().let { + writeUint16(it.size) + write(it) + } + + writeUint64(i.mint.batch) + writeBigInteger(i.mint.amount) + } + + } + + private fun encodeOutput(output: Output) { + write(byteArrayOf(0x00, output.type.value.toByte())) + writeBigInteger(output.amount) + + writeUint16(output.keys.size) + for (key in output.keys) { + write(key.hexStringToByteArray()) + } + + write(output.mask?.hexStringToByteArray() ?: ByteArray(32)) + + val script = output.script?.hexStringToByteArray() ?: ByteArray(0) + writeUint16(script.size) + write(script) + + if (output.withdrawal == null) { + write(empty) + } else { + val w = output.withdrawal + + write(magic) + + w.address.toByteArray().let { + writeUint16(it.size) + write(it) + } + + w.tag.toByteArray().let { + writeUint16(it.size) + write(it) + } + } + } + + fun toHexString(): String { + return buffer.snapshot().hex() + } + + fun encodeSignature(sig: Map) { + val ss = sig.entries.toList().sortedBy { it.key } + writeUint16(ss.size) + for (s in ss) { + writeUint16(s.key) + write(s.value.hexStringToByteArray()) + } + } + +} \ No newline at end of file diff --git a/library/src/main/kotlin/one/mixin/bot/safe/tx/Recipient.kt b/library/src/main/kotlin/one/mixin/bot/safe/tx/Recipient.kt new file mode 100644 index 0000000..722eecb --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/safe/tx/Recipient.kt @@ -0,0 +1,40 @@ +package one.mixin.bot.safe.tx + +import com.ionspin.kotlin.bignum.decimal.BigDecimal +import com.ionspin.kotlin.bignum.integer.BigInteger + +sealed interface TransactionRecipient { + + val amount: String + + fun amountInEthUnit(decimals: Int = 8): BigInteger { + val a = BigDecimal.parseString(amount) + return (a * BigDecimal.TEN.pow(decimals)).toBigInteger() + } + + data class Withdrawal( + val destination: String, + val tag: String?, + override val amount: String, + ) : TransactionRecipient + + data class User( + val members: List, + override val amount: String, + val threshold: Int, + ) : TransactionRecipient { + val script: String + get() = encodeScript(threshold) + } +} + +private fun encodeScript(threshold: Int): String { + var s = threshold.toString(16) + if (s.length == 1) { + s = "0$s" + } + if (s.length > 2) { + throw Exception("invalid threshold. $threshold") + } + return "fffe$s" +} \ No newline at end of file diff --git a/library/src/main/kotlin/one/mixin/bot/safe/tx/Transaction.kt b/library/src/main/kotlin/one/mixin/bot/safe/tx/Transaction.kt new file mode 100644 index 0000000..d641ba3 --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/safe/tx/Transaction.kt @@ -0,0 +1,162 @@ +package one.mixin.bot.safe.tx + +import com.ionspin.kotlin.bignum.integer.BigInteger +import one.mixin.bot.extension.hexStringToByteArray +import one.mixin.bot.extension.toHex +import one.mixin.bot.util.Ed25519Util +import one.mixin.bot.util.blake3 +import one.mixin.bot.util.sha512 +import one.mixin.bot.util.toBytesLE +import one.mixin.bot.vo.GhostKey + + +private const val TX_VERSION: Byte = 0x05 + +private typealias SafeOutput = one.mixin.bot.vo.safe.Output + +data class Transaction( + val version: Byte = TX_VERSION, // kernel asset id + val asset: String, + val extra: String, + val inputs: List, + val outputs: List, + val reference: List, +) { + companion object { + fun build( + utxos: List, + recipients: List, + ghostsKeys: List, + extra: String, + reference: String? = null + ): Transaction { + require(utxos.isNotEmpty()) { "utxos is empty" } + require(recipients.isNotEmpty()) { "recipients is empty" } + require(recipients.size == ghostsKeys.size) { "recipients size not match ghostsKeys size" } + + val asset = utxos[0].asset + + val inputs = mutableListOf() + + for (utxo in utxos) { + require(utxo.asset == asset) { "utxo asset not match. ${utxo.asset} $asset" } + inputs.add(Input(utxo.transactionHash, utxo.outputIndex)) + } + + val outputs = recipients.mapIndexed { index, recipient -> + when (recipient) { + is TransactionRecipient.User -> Output( + OutputType.Script, recipient.amountInEthUnit(), + keys = ghostsKeys[index]!!.keys, + mask = ghostsKeys[index]!!.mask, + script = recipient.script, + ) + + is TransactionRecipient.Withdrawal -> Output( + OutputType.WithdrawalSubmit, recipient.amountInEthUnit(), + withdrawal = WithdrawalData( + recipient.destination, + recipient.tag ?: "", + ), + ) + + } + } + + return Transaction( + asset = asset, + extra = extra, + inputs = inputs, + outputs = outputs, + reference = listOfNotNull(reference), + ) + } + + } + + fun encodeToString(sigs: List> = emptyList()): String { + val encoder = Encoder() + encoder.encodeTransaction(this) + encoder.writeUint16(sigs.size) + for (sig in sigs) { + encoder.encodeSignature(sig) + } + return encoder.toHexString() + } + + fun sign(views: List, utxos: List, privateKey: String): String { + val raw = encodeToString() + val msg = raw.hexStringToByteArray().blake3() + + val spenty = privateKey.substring(0, 64).hexStringToByteArray().sha512() + + val y = Ed25519Util.setBytesWithClamping(spenty.sliceArray(0 until 32)) + + val signaturesMap = mutableListOf>() + + inputs.forEachIndexed { i, input -> + + val view = views[i] + val utxo = utxos[i] + + require(utxo.outputIndex == input.index) { "utxo output i not match" } + require(utxo.transactionHash == input.hash) { "utxo transaction hash not match" } + + val x = Ed25519Util.setCanonicalBytes(view.hexStringToByteArray()) + val t = Ed25519Util.scalarAdd(x, y) + + val key = t.toBytesLE() + val public = Ed25519Util.publicKey(key) + val index = utxo.keys.indexOf(public.toHex()) + require(index >= 0) { "public key not found in utxo keys" } + + val sig = Ed25519Util.sign(msg, key).toHex() + val sigs = mapOf(index to sig) + signaturesMap.add(sigs) + } + + return encodeToString(signaturesMap) + + } + +} + +data class Input( + val hash: String, + val index: Int, + val genesis: String? = null, + val deposit: DepositData? = null, + val mint: MintData? = null, +) + +data class DepositData( + val chain: String, + val assetKey: String, + val transaction: String, + val index: ULong, + val amount: BigInteger, +) + +data class MintData( + val group: String, + val batch: ULong, + val amount: BigInteger, +) + +enum class OutputType(val value: UByte) { + Script(0x00u), WithdrawalSubmit(0xa1u), UNKNOWN(0xffu), +} + +data class Output( + val type: OutputType, + val amount: BigInteger, + val keys: List = listOf(), + val withdrawal: WithdrawalData? = null, + val script: String? = null, + val mask: String? = null, +) + +data class WithdrawalData( + val address: String, + val tag: String, +) \ No newline at end of file diff --git a/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt b/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt index d70cd41..515c2d5 100644 --- a/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt +++ b/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt @@ -11,6 +11,7 @@ import one.mixin.bot.util.keccak.extensions.digestKeccak import one.mixin.eddsa.Ed25519Sign import one.mixin.eddsa.Field25519 import one.mixin.eddsa.KeyPair.Companion.newKeyPair +import org.bouncycastle.crypto.digests.Blake3Digest import org.bouncycastle.jce.provider.BouncyCastleProvider import org.whispersystems.curve25519.Curve25519 import java.security.KeyFactory @@ -104,6 +105,11 @@ fun ByteArray.sha256(): ByteArray { return md.digest(this) } +fun ByteArray.sha512(): ByteArray { + val md = MessageDigest.getInstance("SHA-512") + return md.digest(this) +} + fun String.sha3Sum256(): ByteArray { return digestKeccak(KeccakParameter.SHA3_256) } @@ -112,6 +118,14 @@ fun ByteArray.sha3Sum256(): ByteArray { return digestKeccak(KeccakParameter.SHA3_256) } +fun ByteArray.blake3(): ByteArray { + val md = Blake3Digest(32) + md.update(this, 0, this.size) + val out = ByteArray(md.digestSize) + md.doFinal(out, 0) + return out +} + fun decryptPinToken( serverPublicKey: ByteArray, privateKey: ByteArray, @@ -210,9 +224,8 @@ private fun stripRsaPrivateKeyHeaders(privatePem: String): String { val strippedKey = StringBuilder() val lines = privatePem.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() lines.filter { line -> - !line.contains("BEGIN RSA PRIVATE KEY") && - !line.contains("END RSA PRIVATE KEY") && line.trim { it <= ' ' }.isNotEmpty() - } - .forEach { line -> strippedKey.append(line.trim { it <= ' ' }) } + !line.contains("BEGIN RSA PRIVATE KEY") && !line.contains("END RSA PRIVATE KEY") && line.trim { it <= ' ' } + .isNotEmpty() + }.forEach { line -> strippedKey.append(line.trim { it <= ' ' }) } return strippedKey.toString().trim { it <= ' ' } } diff --git a/library/src/main/kotlin/one/mixin/bot/util/Ed25519.kt b/library/src/main/kotlin/one/mixin/bot/util/Ed25519.kt new file mode 100644 index 0000000..e35bec0 --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/Ed25519.kt @@ -0,0 +1,149 @@ +package one.mixin.bot.util + +import com.ionspin.kotlin.bignum.integer.BigInteger +import com.ionspin.kotlin.bignum.integer.Sign +import okio.Buffer +import okio.ByteString.Companion.toByteString +import one.mixin.eddsa.Ed25519 +import one.mixin.eddsa.Field25519 +import kotlin.experimental.and +import kotlin.experimental.or + + +private fun ByteArray.toNumberLE(): BigInteger { + return BigInteger.fromByteArray(this.reversedArray(), Sign.POSITIVE) +} + +internal fun BigInteger.toBytesLE(size: Int = 32): ByteArray { + val bytes = this.toByteArray().reversedArray() + return if (bytes.size == size) { + bytes + } else { + bytes.copyOf(size) + } + +} + +internal object Ed25519Util { + + // 2^252 + 27742317777372353535851937790883648493 + private val L = + BigInteger.parseString("7237005577332262213973186563042994240857116359379907606001950938285454250989") + + // The order of the generator as unsigned bytes in little endian order. + // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748) + private val GROUP_ORDER = byteArrayOf( + 0xed.toByte(), + 0xd3.toByte(), + 0xf5.toByte(), + 0x5c.toByte(), + 0x1a.toByte(), + 0x63.toByte(), + 0x12.toByte(), + 0x58.toByte(), + 0xd6.toByte(), + 0x9c.toByte(), + 0xf7.toByte(), + 0xa2.toByte(), + 0xde.toByte(), + 0xf9.toByte(), + 0xde.toByte(), + 0x14.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x10.toByte() + ) + + fun setBytesWithClamping(bytes: ByteArray): BigInteger { + require(bytes.size == 32) { "invalid SetBytesWithClamping input length: ${bytes.size}" } + + val wideBytes = bytes.copyOf(64) + + wideBytes[0] = wideBytes[0] and 248.toByte() + wideBytes[31] = wideBytes[31] and 127.toByte() + wideBytes[31] = wideBytes[31] or 64.toByte() + + val numberLE = wideBytes.sliceArray(0 until 32).toNumberLE() + return numberLE % L + } + + fun setUniformBytes(bytes: ByteArray): BigInteger { + require(bytes.size == 64) { "invalid SetUniformBytes input length: ${bytes.size}" } + return bytes.toNumberLE() % L + } + + private fun isReduced(s: ByteArray): Boolean { + for (j in Field25519.FIELD_LEN - 1 downTo 0) { // compare unsigned bytes + val a = s[j].toInt() and 0xff + val b = GROUP_ORDER[j].toInt() and 0xff + if (a != b) { + return a < b + } + } + return false + } + + fun setCanonicalBytes(bytes: ByteArray): BigInteger { + require(bytes.size == 32) { "invalid SetCanonicalBytes input length: ${bytes.size}" } + if (!isReduced(bytes)) { + throw IllegalArgumentException("invalid SetCanonicalBytes input") + } + return bytes.toNumberLE() + } + + fun scalarAdd(a: BigInteger, b: BigInteger): BigInteger { + return (a + b) % L + } + + fun publicKey(privateKey: ByteArray): ByteArray { + val x = setCanonicalBytes(privateKey) + val y = Ed25519.scalarMultWithBase(x.toBytesLE(), true) + return y.toBytes() + } + + + fun sign(message: ByteArray, privateKey: ByteArray): ByteArray { + val digest1 = privateKey.sha512() + val messageDigest = Buffer().apply { + write(digest1.sliceArray(32 until 64)) + write(message) + }.sha512().toByteArray() + + val z = setUniformBytes(messageDigest) + val r = Ed25519.scalarMultWithBase(z.toBytesLE(64), true).toBytes().copyOfRange(0, Field25519.FIELD_LEN) + + val pub = publicKey(privateKey) + + val hramDigest = Buffer().apply { + write(r) + write(pub) + write(message) + }.sha512().toByteArray() + + val x = setUniformBytes(hramDigest) + val y = setCanonicalBytes(privateKey) + + val s = (x * y + z) % L + + return Buffer().apply { + write(r) + write(s.toBytesLE()) + }.snapshot().toByteArray() + + } + + +} \ No newline at end of file diff --git a/library/src/main/kotlin/one/mixin/bot/vo/Utxo.kt b/library/src/main/kotlin/one/mixin/bot/vo/Utxo.kt deleted file mode 100644 index 51292e8..0000000 --- a/library/src/main/kotlin/one/mixin/bot/vo/Utxo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package one.mixin.bot.vo - -data class Utxo( - val hash: String, - val amount: String, - val index: Int = 1, -) diff --git a/library/src/main/kotlin/one/mixin/bot/vo/safe/MixAddress.kt b/library/src/main/kotlin/one/mixin/bot/vo/safe/MixAddress.kt index 4bcb11a..7c7d5c6 100644 --- a/library/src/main/kotlin/one/mixin/bot/vo/safe/MixAddress.kt +++ b/library/src/main/kotlin/one/mixin/bot/vo/safe/MixAddress.kt @@ -1,7 +1,5 @@ package one.mixin.bot.vo.safe -import kernel.Address -import kernel.Kernel import one.mixin.bot.extension.isUUID import one.mixin.bot.util.UUIDUtils import one.mixin.bot.util.decodeBase58 @@ -9,6 +7,7 @@ import one.mixin.bot.util.encodeToBase58String import one.mixin.bot.util.sha3Sum256 const val MixAddressPrefix = "MIX" +const val MainAddressPrefix = "XIN" private const val MixAddressVersion = 0x2.toByte() data class MixAddress( @@ -16,7 +15,7 @@ data class MixAddress( val threshold: Byte, ) { val uuidMembers = mutableListOf() - val xinMembers = mutableListOf
() + val xinMembers = mutableListOf() companion object { fun newUuidMixAddress( @@ -34,16 +33,9 @@ data class MixAddress( fun newMainnetMixAddress( members: List, threshold: Int, - ): MixAddress? { + ): MixAddress { return MixAddress(MixAddressVersion, threshold.toByte()).apply { - for (m in members) { - try { - xinMembers.add(Kernel.newMainAddressFromString(m)) - } catch (e: Exception) { - println("newMainAddressFromString with $m meet $e") - return null - } - } + xinMembers.addAll(members) } } } @@ -52,7 +44,7 @@ data class MixAddress( return if (uuidMembers.size > 0) { uuidMembers } else { - xinMembers.map { it.string() } + xinMembers } } @@ -74,7 +66,7 @@ data class MixAddress( } payload += len.toByte() for (x in xinMembers) { - payload += (x.publicSpendKey() + x.publicViewkey()) + payload += x.mainnetAddressToPublic() } } val data = MixAddressPrefix.toByteArray() + payload @@ -84,16 +76,40 @@ data class MixAddress( } } + +// [return] ByteArray, size 64. [0,32) is publicSpendKey, [32,64) is publicViewKey +fun String.mainnetAddressToPublic(): ByteArray { + if (!startsWith(MainAddressPrefix)) { + throw Exception("invalid address network") + } + val data = substring(MainAddressPrefix.length).decodeBase58() + if (data.size != 68) { + throw Exception("invalid address format") + } + val payload = data.sliceArray(0 until 64) + val checksum = (MainAddressPrefix.toByteArray() + payload).sha3Sum256() + if (!checksum.sliceArray(0..3).contentEquals(data.sliceArray(64 until 68))) { + throw Exception("invalid address checksum") + } + return payload +} + +fun ByteArray.publicToMainnetAddress(): String { + val checksum = (MainAddressPrefix.toByteArray() + this).sha3Sum256() + + val data = this + checksum.sliceArray(0..3) + return MainAddressPrefix + data.encodeToBase58String() +} + fun String.toMixAddress(): MixAddress? { if (!this.startsWith(MixAddressPrefix)) return null - val data = - try { - this.removePrefix(MixAddressPrefix).decodeBase58() - } catch (e: Exception) { - println("decodeBase58 with $this meet $e") - return null - } + val data = try { + this.removePrefix(MixAddressPrefix).decodeBase58() + } catch (e: Exception) { + println("decodeBase58 with $this meet $e") + return null + } if (data.size < 3 + 16 + 4) return null val payload = data.sliceArray(0..data.size - 5) @@ -116,14 +132,14 @@ fun String.toMixAddress(): MixAddress? { mixAddress.uuidMembers.add(id) } } + 64 * total -> { for (i in 0.. { return null } diff --git a/library/src/main/kotlin/one/mixin/bot/vo/safe/SignResult.kt b/library/src/main/kotlin/one/mixin/bot/vo/safe/SignResult.kt deleted file mode 100644 index 2cc5d14..0000000 --- a/library/src/main/kotlin/one/mixin/bot/vo/safe/SignResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package one.mixin.bot.vo.safe - -import kernel.Utxo - -data class SignResult( - val raw: String, - val change: Utxo?, -) diff --git a/library/src/main/kotlin/one/mixin/bot/vo/safe/UtxoWrapper.kt b/library/src/main/kotlin/one/mixin/bot/vo/safe/UtxoWrapper.kt deleted file mode 100644 index db4ba09..0000000 --- a/library/src/main/kotlin/one/mixin/bot/vo/safe/UtxoWrapper.kt +++ /dev/null @@ -1,36 +0,0 @@ -package one.mixin.bot.vo.safe - -import com.google.gson.Gson -import one.mixin.bot.vo.Utxo - -data class UtxoWrapper(val outputs: List) { - val keys: List> by lazy { generateKeys() } - val ids: List by lazy { generateIds() } - val lastOutput by lazy { outputs.last() } - - private val gson = Gson() - - val formatKeys: String = gson.toJson(keys) - - val input: ByteArray = gson.toJson(generateUtxos()).toByteArray() - - val firstSequence = outputs.first().sequence - - private fun generateKeys(): List> { - return outputs.map { it.keys } - } - - private fun generateIds(): List { - return outputs.map { it.outputId } - } - - private fun generateUtxos(): List { - return outputs.map { output -> - Utxo( - hash = output.transactionHash, - amount = output.amount, - index = output.outputIndex, - ) - } - } -} diff --git a/library/src/test/kotlin/one/mixin/bot/JNITest.kt b/library/src/test/kotlin/one/mixin/bot/JNITest.kt deleted file mode 100644 index e6184fe..0000000 --- a/library/src/test/kotlin/one/mixin/bot/JNITest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package one.mixin.bot - -import kernel.Kernel -import one.mixin.bot.extension.base64Decode -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.test.Test -import kotlin.test.assertEquals - -class JNITest { - @Test fun testLoadAndUse() { - val currentRelativePath: Path = Paths.get("") - val s: String = currentRelativePath.toAbsolutePath().toString() - println("Current absolute path is: $s") - - val os = System.getProperty("os.name") - val arch = System.getProperty("os.arch") - println("os: $os, arch: $arch") - - val platform = - when { - OS.isLinux() -> "linux" - OS.isMacOSX() -> "darwin" - OS.isWindows() -> "windows" - else -> throw IllegalArgumentException("not supported os $os") - } - - System.load("$s/libs/$platform/amd64/libgojni.so") - - val assetId = "5b9d576914e71e2362f89bb867eb69084931eb958f9a3622d776b861602275f4" - val amount = "1" - val address = "TV9mvdJv61mVtEoTY5h6kQtrvrULcFfadM" - val tag = "" - val feeAmount = "" - val feeKeys = "" - val feeMask = "" - val inputs = "W3siYW1vdW50IjoiMTAiLCJoYXNoIjoiZjY0MTA2OWU0Y2NjZTYyZTcwZGU1OTI1M2MxMGY2YzUzNzRlODdjMzYxYmMzMjhiODIwNzE5ZDc5MTRmYTJlZCIsImluZGV4IjowfV0".base64Decode() - val changeKeys = "5a21338c6e0e731afec7b87fb52447e095fb47b602bb89b0eb6ee68e8252623a" - val changeMask = "2c911e2b2cc4f847baadddee1bb4be927a3239a436c3e28f9e736f8d436f9311" - val extra = "" - val tx = Kernel.buildWithdrawalTx(assetId, amount, address, tag, feeAmount, feeKeys, feeMask, inputs, changeKeys, changeMask, extra) - println("tx: $tx") - assertEquals(tx.hash, "a6b10f4183f7e8ab358806ad08ed686cdc3b1983dd2612ea7df8f09b52d42bb3") - } -} diff --git a/library/src/test/kotlin/one/mixin/bot/safe/EncoderTest.kt b/library/src/test/kotlin/one/mixin/bot/safe/EncoderTest.kt new file mode 100644 index 0000000..9c3a654 --- /dev/null +++ b/library/src/test/kotlin/one/mixin/bot/safe/EncoderTest.kt @@ -0,0 +1,22 @@ +package one.mixin.bot.safe + +import one.mixin.bot.safe.tx.Encoder +import kotlin.test.Test +import kotlin.test.expect + +class EncoderTest { + + @Test + fun `encode sig`() { + val encoder = Encoder() + encoder.encodeSignature( + mapOf( + 0 to "fde63b999d519394b3ba8a99a9f1d44bc91c2ee73d472d2085fa222925732889159042b126b4436766d6d3308ee541c50fc1b9b8b83701ac534c68e7f4d0f50c" + ) + ) + expect("00010000fde63b999d519394b3ba8a99a9f1d44bc91c2ee73d472d2085fa222925732889159042b126b4436766d6d3308ee541c50fc1b9b8b83701ac534c68e7f4d0f50c") { + encoder.toHexString() + } + } + +} \ No newline at end of file diff --git a/library/src/test/kotlin/one/mixin/bot/safe/MixAddressTest.kt b/library/src/test/kotlin/one/mixin/bot/safe/MixAddressTest.kt new file mode 100644 index 0000000..8dacac2 --- /dev/null +++ b/library/src/test/kotlin/one/mixin/bot/safe/MixAddressTest.kt @@ -0,0 +1,75 @@ +package one.mixin.bot.safe + +import one.mixin.bot.vo.safe.MixAddress +import one.mixin.bot.vo.safe.toMixAddress +import kotlin.test.Test + +class MixAddressTest { + + @Test + fun `test 1`() { + val members = listOf("67a87828-18f5-46a1-b6cc-c72a97a77c43") + val address = MixAddress.newUuidMixAddress(members, 1).toString() + assert(address == "MIX3QEeg1WkLrjvjxyMQf6Xc8dxs81tpPc") + + val ma = "MIX3QEeg1WkLrjvjxyMQf6Xc8dxs81tpPc".toMixAddress() + assert(ma != null) + assert(ma!!.uuidMembers.joinToString(",") == members.joinToString(",")) + assert(ma.threshold == 1.toByte()) + } + + @Test + fun `test 2`() { + val members = listOf( + "67a87828-18f5-46a1-b6cc-c72a97a77c43", + "c94ac88f-4671-3976-b60a-09064f1811e8", + "c6d0c728-2624-429b-8e0d-d9d19b6592fa", + "67a87828-18f5-46a1-b6cc-c72a97a77c43", + "c94ac88f-4671-3976-b60a-09064f1811e8", + "c6d0c728-2624-429b-8e0d-d9d19b6592fa", + "67a87828-18f5-46a1-b6cc-c72a97a77c43", + ) + val address = MixAddress.newUuidMixAddress(members, 4).toString() + assert( + address == "MIX4fwusRK88p5GexHWddUQuYJbKMJTAuBvhudgahRXKndvaM8FdPHS2Hgeo7DQxNVoSkKSEDyZeD8TYBhiwiea9PvCzay1A9Vx1C2nugc4iAmhwLGGv4h3GnABeCXHTwWEto9wEe1MWB49jLzy3nuoM81tqE2XnLvUWv" + ) + val ma = + "MIX4fwusRK88p5GexHWddUQuYJbKMJTAuBvhudgahRXKndvaM8FdPHS2Hgeo7DQxNVoSkKSEDyZeD8TYBhiwiea9PvCzay1A9Vx1C2nugc4iAmhwLGGv4h3GnABeCXHTwWEto9wEe1MWB49jLzy3nuoM81tqE2XnLvUWv".toMixAddress(); + assert(ma != null) + assert(ma!!.uuidMembers.joinToString() == members.joinToString()) + } + + + @Test + fun `test 3`() { + val members = listOf( + "XIN3BMNy9pQyj5XWDJtTbaBVE2zQ66zBo2weyc43iL286asdqwApWswAzQC5qba26fh3fzHK9iMoxyx1q3Lgj45KJftzGD9q" + ) + val address = MixAddress.newMainnetMixAddress(members, 1).toString() + assert( + address == "MIXPYWwhjxKsbFRzAP2Dcb2mMjj7sQQo4MpCSv3NYaYCdQ2kEcbcimpPT81gaxtuNhunLWPx7Sv7fawjZ8DhRmEj8E2hrQM4Z6e" + ) + val ma = + "MIXPYWwhjxKsbFRzAP2Dcb2mMjj7sQQo4MpCSv3NYaYCdQ2kEcbcimpPT81gaxtuNhunLWPx7Sv7fawjZ8DhRmEj8E2hrQM4Z6e".toMixAddress(); + assert(ma != null) + assert(ma!!.xinMembers.joinToString() == members.joinToString()) + } + + @Test + fun `test 4`() { + val members = listOf( + "XINGNzunRUMmKGqDhnf1MT8tR7ek6ozg2V6dXFHCCg3tndnSRcAdzET8Fw4ktcQKshzteDmyV2RE8aFiKPz8ewrvsj3s7fvC", + "XINMd9kCbxEoEetZuDM8gGJS11X3TVrRLwzhnqgMr65qjJBkCncNqSAngESpC7Hddnsw1D9Jo2QJakbFPr8WyrM6VkskGkB8", + "XINLM7VuMYSjvKiEQPyLpaG7NDLDPngWWFBZpVJjhGamMsgPbmeSsGs3fQzNoqSr6syBTyLM3i69T7iSN8Tru7aQadiKLkSV", + ) + val address = MixAddress.newMainnetMixAddress(members, 2).toString() + assert( + address == "MIXBCirWksVv9nuphqbtNRZZvwKsXHHMUnB5hVrVY1P7f4eBdLpDoLwiQoHYPvXia2wFepnX6hJwTjHybzBiroWVEMaFHeRFfLpcU244tzRM8smak9iRAD4PJRHN1MLHRWFtErottp9t7piaRVZBzsQXpSsaSgagj93voQdUuXhuQGZNj3Fme5YYMHfJBWjoRFHis4mnhBgxkyEGRUHAVYnfej2FhrypJmMDu74irRTdj2xjQYr6ovBJSUBYDBcvAyLPE3cEKc4JsPz7b9" + ) + val ma = + "MIXBCirWksVv9nuphqbtNRZZvwKsXHHMUnB5hVrVY1P7f4eBdLpDoLwiQoHYPvXia2wFepnX6hJwTjHybzBiroWVEMaFHeRFfLpcU244tzRM8smak9iRAD4PJRHN1MLHRWFtErottp9t7piaRVZBzsQXpSsaSgagj93voQdUuXhuQGZNj3Fme5YYMHfJBWjoRFHis4mnhBgxkyEGRUHAVYnfej2FhrypJmMDu74irRTdj2xjQYr6ovBJSUBYDBcvAyLPE3cEKc4JsPz7b9".toMixAddress() + assert(ma != null) + assert(ma!!.xinMembers.joinToString() == members.joinToString()) + + } +} \ No newline at end of file diff --git a/library/src/test/kotlin/one/mixin/bot/safe/TransactionTest.kt b/library/src/test/kotlin/one/mixin/bot/safe/TransactionTest.kt new file mode 100644 index 0000000..a7fe58d --- /dev/null +++ b/library/src/test/kotlin/one/mixin/bot/safe/TransactionTest.kt @@ -0,0 +1,152 @@ +package one.mixin.bot.safe + +import com.ionspin.kotlin.bignum.integer.BigInteger +import one.mixin.bot.safe.tx.Input +import one.mixin.bot.safe.tx.Output +import one.mixin.bot.safe.tx.OutputType +import one.mixin.bot.safe.tx.Transaction +import kotlin.system.measureNanoTime +import kotlin.test.Test +import kotlin.test.expect + +class TransactionTest { + + @Test + fun `Test for safe transaction signature`() { + val raw1 = + "77770005b9f49cf777dc4d03bc54cd1367eebca319f8603ea1ce18910d09e2c540c630d80001c513ffcc684e9585c76bd76245aa7d2def3b9f147422b59ab91db7852c9d97dd000000000000000000010000000405f5e1000001a6c306c3137c2bf4a8bfc95ea5165f7777020916aa36ee6ec394beb9a1e6a164286d4f092015ea327ba12145edd40ddede1bdd80f777c249128d38176e352a130003fffe010000000000000009746573742d6d656d6f0000" + val raw2 = + "77770005b9f49cf777dc4d03bc54cd1367eebca319f8603ea1ce18910d09e2c540c630d80001c513ffcc684e9585c76bd76245aa7d2def3b9f147422b59ab91db7852c9d97dd000000000000000000010000000405f5e1000001a6c306c3137c2bf4a8bfc95ea5165f7777020916aa36ee6ec394beb9a1e6a164286d4f092015ea327ba12145edd40ddede1bdd80f777c249128d38176e352a130003fffe010000000000000009746573742d6d656d6f000100010000fde63b999d519394b3ba8a99a9f1d44bc91c2ee73d472d2085fa222925732889159042b126b4436766d6d3308ee541c50fc1b9b8b83701ac534c68e7f4d0f50c" + val views = listOf( + "0164ba23d5aa1953132bc0bf5d12d0af7e66de2ba8773701ef135e015f24bb0b" + ) + + val priv = + "7fb3893475a82c85e2b3c8a9a9232eddb36651ac32fa98ae83e6c2f33fb1be84dea64fa32b3b01f9a059142c0e9535a57b69f676790ae64f6d52f9a06d90f11e" + + val tx = Transaction( + asset = "b9f49cf777dc4d03bc54cd1367eebca319f8603ea1ce18910d09e2c540c630d8", + extra = "test-memo", + inputs = listOf( + Input( + hash = "c513ffcc684e9585c76bd76245aa7d2def3b9f147422b59ab91db7852c9d97dd", + index = 0, + ) + ), + outputs = listOf( + Output( + type = OutputType.Script, + amount = BigInteger(10).pow(8), + keys = listOf( + "a6c306c3137c2bf4a8bfc95ea5165f7777020916aa36ee6ec394beb9a1e6a164" + ), + mask = "286d4f092015ea327ba12145edd40ddede1bdd80f777c249128d38176e352a13", + script = "fffe01", + ) + ), + reference = emptyList() + ) + + expect(raw1) { + tx.encodeToString() + } + + val utxos = listOf( + one.mixin.bot.vo.safe.Output( + keys = listOf( + "0b6c1f0d107d9b825b48d2dcd711993ad5e6b0b06501adce5db767711e97551b", + "e0d29cfc66eee800fe3fc2329fd49a93cb11006675fa39db6e64739a79d611ba" + ), + outputIndex = 0, + transactionHash = "c513ffcc684e9585c76bd76245aa7d2def3b9f147422b59ab91db7852c9d97dd", + mask = "", + outputId = "", + asset = "", + sequence = 0, + amount = "1", + receivers = listOf(), + receiversHash = "", + receiversThreshold = 0, + extra = "", + state = "", + createdAt = "", + updatedAt = "", + signedBy = "", + signedAt = "", + spentAt = "", + ) + ) + + expect(raw2) { + tx.sign(views, utxos, priv) + } + + } + + @Test + fun benchmark() { + val views = listOf( + "0164ba23d5aa1953132bc0bf5d12d0af7e66de2ba8773701ef135e015f24bb0b" + ) + val tx = Transaction( + asset = "b9f49cf777dc4d03bc54cd1367eebca319f8603ea1ce18910d09e2c540c630d8", + extra = "test-memo", + inputs = listOf( + Input( + hash = "c513ffcc684e9585c76bd76245aa7d2def3b9f147422b59ab91db7852c9d97dd", + index = 0, + ) + ), + outputs = listOf( + Output( + type = OutputType.Script, + amount = BigInteger(10).pow(8), + keys = listOf( + "a6c306c3137c2bf4a8bfc95ea5165f7777020916aa36ee6ec394beb9a1e6a164" + ), + mask = "286d4f092015ea327ba12145edd40ddede1bdd80f777c249128d38176e352a13", + script = "fffe01", + ) + ), + reference = emptyList() + ) + + val utxos = listOf( + one.mixin.bot.vo.safe.Output( + keys = listOf( + "0b6c1f0d107d9b825b48d2dcd711993ad5e6b0b06501adce5db767711e97551b", + "e0d29cfc66eee800fe3fc2329fd49a93cb11006675fa39db6e64739a79d611ba" + ), + outputIndex = 0, + transactionHash = "c513ffcc684e9585c76bd76245aa7d2def3b9f147422b59ab91db7852c9d97dd", + mask = "", + outputId = "", + asset = "", + sequence = 0, + amount = "1", + receivers = listOf(), + receiversHash = "", + receiversThreshold = 0, + extra = "", + state = "", + createdAt = "", + updatedAt = "", + signedBy = "", + signedAt = "", + spentAt = "", + ) + ) + + + val priv = + "7fb3893475a82c85e2b3c8a9a9232eddb36651ac32fa98ae83e6c2f33fb1be84dea64fa32b3b01f9a059142c0e9535a57b69f676790ae64f6d52f9a06d90f11e" + + val nanoTime = measureNanoTime { + repeat(10000) { + tx.sign(views, utxos, priv) + } + } + println("consume: ${nanoTime.toDouble() / 1000000} ms") + } + +} \ No newline at end of file diff --git a/samples/src/main/java/jvmMain/java/BlazeSample.java b/samples/src/main/java/jvmMain/java/BlazeSample.java index abf8273..1581fa4 100644 --- a/samples/src/main/java/jvmMain/java/BlazeSample.java +++ b/samples/src/main/java/jvmMain/java/BlazeSample.java @@ -4,23 +4,22 @@ import static one.mixin.bot.blaze.BlazeClientKt.sendTextMsg; import java.io.IOException; + import okhttp3.WebSocket; import one.mixin.bot.blaze.BlazeClient; import one.mixin.bot.blaze.BlazeHandler; import one.mixin.bot.blaze.BlazeMsg; import one.mixin.bot.blaze.MsgData; -import one.mixin.bot.extension.Base64ExtensionKt; -import one.mixin.bot.safe.EdKeyPair; -import one.mixin.bot.util.CryptoUtilKt; +import one.mixin.bot.extension.ByteArrayExtensionKt; import org.jetbrains.annotations.NotNull; @SuppressWarnings("SameParameterValue") class BlazeSample { public static void main(String[] args) throws IOException { - EdKeyPair key = CryptoUtilKt.newKeyPairFromPrivateKey(Base64ExtensionKt.base64Decode(privateKey)); + BlazeClient blazeClient = new BlazeClient.Builder() - .configSafeUser(userId, sessionId, key.getPrivateKey(), null, null) + .configSafeUser(BOT_USER_ID, BOT_SESSION_ID, ByteArrayExtensionKt.hexStringToByteArray(BOT_SESSION_PRIVATE_KEY), null, null) .enableDebug() .enableParseData() .enableAutoAck() diff --git a/samples/src/main/java/jvmMain/java/Common.java b/samples/src/main/java/jvmMain/java/Common.java new file mode 100644 index 0000000..21ec126 --- /dev/null +++ b/samples/src/main/java/jvmMain/java/Common.java @@ -0,0 +1,34 @@ +package jvmMain.java; + +import one.mixin.bot.HttpClient; +import one.mixin.bot.extension.ByteArrayExtensionKt; + +public class Common { + + public static final HttpClient botClient = new HttpClient.Builder().configSafeUser( + Config.BOT_USER_ID, + Config.BOT_SESSION_ID, + ByteArrayExtensionKt.hexStringToByteArray(Config.BOT_SESSION_PRIVATE_KEY), + null, + ByteArrayExtensionKt.hexStringToByteArray(Config.BOT_SPEND_KEY) + ).enableDebug().build(); + + public enum Token { + CNB("965e5c6e-434c-3fa9-b780-c50f43cd955c"), + TRON_USDT("b91e18ff-a9ae-3dc7-8679-e935d9a4b34b"), + TRX("25dabac5-056a-48ff-b9f9-f67395dc407c"); + + private final String assetId; + + Token(String assetId) { + this.assetId = assetId; + } + + public String getAssetId() { + return assetId; + } + } + + +} + diff --git a/samples/src/main/java/jvmMain/java/Config.java b/samples/src/main/java/jvmMain/java/Config.java index c7279a6..f5471b9 100644 --- a/samples/src/main/java/jvmMain/java/Config.java +++ b/samples/src/main/java/jvmMain/java/Config.java @@ -1,9 +1,8 @@ package jvmMain.java; final class Config { - final static String pin = "967784"; - final static String userId = "24888245-d200-4907-aff0-1303d94217d5"; - final static String sessionId = "0d89aa10-f559-400e-9323-9d103f9fd49e"; - final static String privateKey = "ZywWUtZxMUxdS6LHSEqVsxFr9Y3pM5OJDx648Hm54Tgn9IoGhUOLbNoQ2nKewVNnCsBF3qOcb4nuiowuxQax4w"; - final static String pinTokenPem = "kMu0ihumvCC0t1sgbD1n1sv8TQLFVje4qWv-Wpy_Ihg"; + final static String BOT_USER_ID = ""; + final static String BOT_SESSION_ID = ""; + final static String BOT_SESSION_PRIVATE_KEY = ""; + final static String BOT_SPEND_KEY = ""; } \ No newline at end of file diff --git a/samples/src/main/java/jvmMain/java/SafeSample.java b/samples/src/main/java/jvmMain/java/SafeSample.java index 76bc186..572581f 100644 --- a/samples/src/main/java/jvmMain/java/SafeSample.java +++ b/samples/src/main/java/jvmMain/java/SafeSample.java @@ -1,46 +1,35 @@ package jvmMain.java; -import kotlin.ExceptionsKt; -import kotlin.collections.CollectionsKt; import one.mixin.bot.HttpClient; import one.mixin.bot.api.MixinResponse; import one.mixin.bot.extension.Base64ExtensionKt; import one.mixin.bot.extension.ByteArrayExtensionKt; -import one.mixin.bot.extension.StringExtensionKt; import one.mixin.bot.extension.TimeExtensionKt; -import one.mixin.bot.safe.*; +import one.mixin.bot.safe.EdKeyPair; +import one.mixin.bot.safe.TipBody; +import one.mixin.bot.safe.TipException; +import one.mixin.bot.safe.TipKt; import one.mixin.bot.util.ByteArrayUtilKt; import one.mixin.bot.util.CryptoUtilKt; import one.mixin.bot.vo.Account; import one.mixin.bot.vo.PinRequest; import one.mixin.bot.vo.User; -import one.mixin.bot.vo.safe.MixAddress; -import one.mixin.bot.vo.safe.MixAddressKt; -import one.mixin.bot.vo.safe.TransactionRecipient; -import one.mixin.bot.vo.safe.TransactionResponse; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.Objects; -import java.util.UUID; -import static jvmMain.java.Sample.*; +import static jvmMain.java.Sample.createUser; +import static jvmMain.java.Sample.userPin; import static one.mixin.bot.SessionKt.encryptPin; import static one.mixin.bot.SessionKt.encryptTipPin; public class SafeSample { public static void main(String[] args) throws Exception { - HttpClient botClient = new HttpClient.Builder().useCNServer().configSafeUser( - Config.userId, - Config.sessionId, - Base64ExtensionKt.base64UrlDecode(Config.privateKey), - Base64ExtensionKt.base64UrlDecode(Config.pinTokenPem), - ByteArrayExtensionKt.hexStringToByteArray(Config.pin) - ).enableDebug().build(); + HttpClient botClient = Common.botClient; - updateFromLegacyPin(botClient); + updateFromLegacyPin(botClient); // Account user = createTipPin(botClient); @@ -68,7 +57,7 @@ private static void updateFromLegacyPin(HttpClient botClient) throws Exception { sessionKey.getPrivateKey(), null, null - ).build(); + ).build(); // decrypt pin token byte[] userPrivateKey = sessionKey.getPrivateKey(); @@ -141,7 +130,7 @@ private static Account createTipPin(HttpClient botClient) throws IOException, Ti new PinRequest( encryptPin(userAesKey, Bytes.concat(keyPair.getPublicKey(), ByteArrayUtilKt.toBeByteArray(1L))), null, null - )).execute().body(); + )).execute().body(); assert response != null; if (response.isSuccess()) { System.out.println("Create tip pin success " + Objects.requireNonNull(response.getData())); @@ -153,31 +142,5 @@ private static Account createTipPin(HttpClient botClient) throws IOException, Ti return TipKt.registerSafe(userClient, user.getUserId(), ByteArrayExtensionKt.toHex(keyPair.getPrivateKey()), ByteArrayExtensionKt.toHex(keyPair.getPrivateKey()), Base64ExtensionKt.base64UrlEncode(userPrivateKey), user.getPinToken()); } - private static void transactionToOne(HttpClient botClient) throws SafeException, IOException { - String asset = StringExtensionKt.assetIdToAsset("965e5c6e-434c-3fa9-b780-c50f43cd955c"); // cnb - MixAddress mixAddress = MixAddress.Companion.newUuidMixAddress(CollectionsKt.listOf("d3bee23a-81d4-462e-902a-22dae9ef89ff"), 1); - assert mixAddress != null; - TransactionRecipient transactionRecipient = new TransactionRecipient(mixAddress, "0.013", null, null); - String trace = UUID.randomUUID().toString(); - System.out.println("trace: " + trace); - List tx = TransactionKt.sendTransaction(botClient, asset, transactionRecipient, trace, ""); - System.out.println(tx); - - try { - TransactionKt.sendTransaction(botClient, asset, transactionRecipient, trace, ""); - } catch (SafeException e) { - System.out.println("use same id should throw exception " + ExceptionsKt.stackTraceToString(e)); - } - } - private static void transactionToMultiple(HttpClient botClient) throws SafeException, IOException { - String asset = StringExtensionKt.assetIdToAsset("965e5c6e-434c-3fa9-b780-c50f43cd955c"); // cnb - MixAddress mixAddress = MixAddressKt.toMixAddress("MIXDLSoouhdcvedoiSzNHNRR4FNqVNwwgHUXkFoApTsz35fBHSNGyZEqGCzWuwDYrrWDwCXiaNcPec4C5cW8tCiE7BUHvs6A9YZ4B6FiFAEYY5Nd1etLA7aE7"); - assert mixAddress != null; - TransactionRecipient transactionRecipient = new TransactionRecipient(mixAddress, "0.012", null, null); - String trace = UUID.randomUUID().toString(); - System.out.println("trace: " + trace); - List tx = TransactionKt.sendTransaction(botClient, asset, transactionRecipient, trace, ""); - System.out.println(tx); - } } diff --git a/samples/src/main/java/jvmMain/java/Sample.java b/samples/src/main/java/jvmMain/java/Sample.java index 6b36b89..c237f4b 100644 --- a/samples/src/main/java/jvmMain/java/Sample.java +++ b/samples/src/main/java/jvmMain/java/Sample.java @@ -1,12 +1,11 @@ package jvmMain.java; -import kotlin.Unit; +import kotlin.text.HexExtensionsKt; import one.mixin.bot.HttpClient; import one.mixin.bot.api.MixinResponse; -import one.mixin.bot.extension.Base64ExtensionKt; +import one.mixin.bot.extension.ByteArrayExtensionKt; import one.mixin.bot.safe.EdKeyPair; import one.mixin.bot.util.ConversationUtil; -import one.mixin.bot.util.CryptoUtilKt; import one.mixin.bot.vo.*; import java.io.IOException; @@ -28,33 +27,20 @@ public class Sample { final static String amount = "0.001"; public static void main(String[] args) { - EdKeyPair key = CryptoUtilKt.newKeyPairFromPrivateKey(Base64ExtensionKt.base64Decode(privateKey)); - byte[] pinToken = decryptPinToken(Base64ExtensionKt.base64Decode(pinTokenPem), key.getPrivateKey()); - HttpClient client = new HttpClient.Builder().configSafeUser(userId, sessionId, key.getPrivateKey(), null, null).enableDebug().enableAutoSwitch().build(); + HttpClient client = new HttpClient.Builder().configSafeUser(BOT_USER_ID, BOT_SESSION_ID, + ByteArrayExtensionKt.hexStringToByteArray(BOT_SESSION_PRIVATE_KEY), null, null).enableDebug().enableAutoSwitch().build(); try { - utxo(client); EdKeyPair sessionKey = generateEd25519KeyPair(); String sessionSecret = base64Encode(sessionKey.getPublicKey()); // searchUser(client); - getOutputs(client); - User user = createUser(client, sessionSecret); assert user != null; // decrypt pin token byte[] userAesKey = calculateAgreement(Objects.requireNonNull(base64Decode(user.getPinToken())), privateKeyToCurve25519(sessionKey.getPrivateKey())); - // get ticker - getTicker(client); - - // get fiats - getFiats(client); - - // get BTC fee - getFee(client); - HttpClient userClient = new HttpClient.Builder().configSafeUser(user.getUserId(), user.getSessionId(), sessionKey.getPrivateKey(), null, null).enableDebug().enableAutoSwitch().build(); // create user's pin @@ -62,70 +48,20 @@ public static void main(String[] args) { pinVerifyCall(userClient, userAesKey, userPin); - // bot transfer to user - transferToUser(client, user.getUserId(), pinToken, pin); - Thread.sleep(2000); - getAsset(userClient); - - // Create address - String addressId = createAddress(userClient, userAesKey); - - // withdrawal - withdrawalToAddress(userClient, addressId, userAesKey); - - // Delete address - deleteAddress(userClient, addressId, userAesKey); // Send text message sendTextMessage(client, "639ec50a-d4f1-4135-8624-3c71189dcdcc", "Test message"); - createConversationAndSendMessage(client, userId); - - List receivers = new ArrayList<>(); - receivers.add("00c5a4ae-dcdc-48db-ab8e-a7eef69b441d"); - receivers.add("087e91ff-7169-451a-aaaa-5b3297411a4b"); - receivers.add("105f6e8b-d249-4b4d-9beb-e03cefaebc37"); - transactions(client, receivers, pinToken, pin); - - transactionsOpponentKey(client, "XINQTmRReDuPEUAVEyDyE2mBgxa1ojVRAvpYcKs5nSA7FDBBfAEeVRn8s9vAm3Cn1qzQ7JtjG62go4jSJU6yWyRUKHpamWAM", pinToken, pin); + createConversationAndSendMessage(client, BOT_USER_ID); - networkSnapshot(client, "c8e73a02-b543-4100-bd7a-879ed4accdfc"); - networkSnapshots(client, CNB_assetId, null, 10, null); - - readGhostKey(client); } catch (InterruptedException | IOException e) { System.out.println(e.getMessage()); } } - private static void utxo(HttpClient client) throws IOException { -// JsonObject response = client.getExternalService().getUtxoCall("b6afed179a8192513990e29953e3a6875eab53050b1e174d5c83ab76bbbd4b29",0).execute().body(); -// assert response != null; -// System.out.printf("%s%n", Utxo.Companion.fromJson(response.getAsJsonObject("data")).getHash()); - } - - private static String createAddress(HttpClient client, byte[] userAesKey) throws IOException { - MixinResponse
addressResponse = client.getAddressService().createAddressesCall(new AddressRequest(Sample.CNB_assetId, - "0x45315C1Fd776AF95898C77829f027AFc578f9C2B", - null, - "label", - Objects.requireNonNull(encryptPin( - userAesKey, - Sample.userPin)) - )).execute().body(); - assert addressResponse != null; - if (addressResponse.isSuccess()) { - String addressId = Objects.requireNonNull(addressResponse.getData()).getAddressId(); - System.out.printf("Create address success: %s%n", addressId); - return addressId; - } else { - return null; - } - } - - private static void pinVerifyCall(HttpClient client,byte[] userAesKey,String pin) throws IOException{ + private static void pinVerifyCall(HttpClient client, byte[] userAesKey, String pin) throws IOException { MixinResponse pinResponse = client.getUserService().pinVerifyCall(new PinRequest(Objects.requireNonNull(encryptPin(userAesKey, pin)), null, null)).execute().body(); if (pinResponse.isSuccess()) { System.out.printf("Pin verifyCall success %s%n", Objects.requireNonNull(pinResponse.getData()).getUserId()); @@ -176,104 +112,11 @@ static void createPin(HttpClient client, byte[] userAesKey) throws IOException { } } - private static void transferToUser(HttpClient client, String userId, byte[] aseKey, String pin) throws IOException { - MixinResponse transferResponse = client.getSnapshotService().transferCall( - new TransferRequest(Sample.CNB_assetId, userId, Sample.amount, encryptPin(aseKey, pin, System.currentTimeMillis() * 1_000_000), null, null, null)).execute().body(); - assert transferResponse != null; - if (transferResponse.isSuccess()) { - System.out.printf("Transfer success: %s%n", Objects.requireNonNull(transferResponse.getData()).getSnapshotId()); - } else { - System.out.println("Transfer failure"); - } - } - - private static void getAsset(HttpClient client) throws IOException { - // Get asset - MixinResponse assetResponse = client.getAssetService().getAssetCall(Sample.CNB_assetId).execute().body(); - assert assetResponse != null; - if (assetResponse.isSuccess()) { - System.out.printf("Assets %s: %s%n", Objects.requireNonNull(assetResponse.getData()).getSymbol(), Objects.requireNonNull(assetResponse.getData()).getBalance()); - } else { - System.out.println("Transfer failure"); - } - } - - private static void getOutputs(HttpClient client) throws IOException { - // Get output - MixinResponse> outputResponse = client.getUserService().multisigsOutputsCall( - null, null, null, null, null, null - ).execute().body(); - assert outputResponse != null; - if (outputResponse.isSuccess()) { - System.out.printf("Output: %d%n", Objects.requireNonNull(outputResponse.getData()).size()); - } else { - System.out.println("Output failure"); - } - } - - - private static void withdrawalToAddress(HttpClient client, String addressId, byte[] userAesKey) throws IOException { - MixinResponse withdrawalsResponse = client.getSnapshotService().withdrawalsCall(new WithdrawalRequest(addressId, Sample.amount, Objects.requireNonNull(encryptPin( - userAesKey, - Sample.userPin - )), UUID.randomUUID().toString(), "withdrawal test")).execute().body(); - assert withdrawalsResponse != null; - if (withdrawalsResponse.isSuccess()) { - addressId = Objects.requireNonNull(withdrawalsResponse.getData()).getSnapshotId(); - System.out.printf("Withdrawal success: %s%n", addressId); - } else { - System.out.println("Withdrawal failure"); - } - } - - private static void deleteAddress(HttpClient client, String addressId, byte[] userAesKey) throws IOException { - MixinResponse deleteResponse = client.getAddressService().deleteCall(addressId, new Pin(Objects.requireNonNull(encryptPin( - userAesKey, - Sample.userPin - )))).execute().body(); - assert deleteResponse != null; - if (deleteResponse.isSuccess()) { - System.out.printf("Delete success: %s%n", addressId); - - } else { - System.out.println("Delete failure"); - } - } - - private static void getFiats(HttpClient client) throws IOException { - MixinResponse> fiatsResponse = client.getAssetService().getFiatsCall().execute().body(); - assert fiatsResponse != null; - if (fiatsResponse.isSuccess()) { - System.out.printf("Fiats success: %f%n", Objects.requireNonNull(fiatsResponse.getData()).get(0).getRate()); - } else { - System.out.println("Fiats failure"); - } - } - - private static void getFee(HttpClient client) throws IOException { - MixinResponse feeResponse = client.getAssetService().assetsFeeCall(BTC_assetId).execute().body(); - assert feeResponse != null; - if (feeResponse.isSuccess()) { - System.out.printf("Fee success: %s%n", Objects.requireNonNull(feeResponse.getData()).getAmount()); - } else { - System.out.println("Fee failure"); - } - } - - private static void getTicker(HttpClient client) throws IOException { - MixinResponse tickerResponse = client.getAssetService().tickerCall(BTC_assetId, null).execute().body(); - assert tickerResponse != null; - if (tickerResponse.isSuccess()) { - System.out.printf("Ticker success: %s%n", Objects.requireNonNull(tickerResponse.getData())); - } else { - System.out.println("Ticker failure"); - } - } private static void sendTextMessage(HttpClient client, String recipientId, String text) throws IOException { List messageRequests = new ArrayList<>(); messageRequests.add(new MessageRequest( - ConversationUtil.Companion.generateConversationId(userId, recipientId), + ConversationUtil.Companion.generateConversationId(BOT_USER_ID, recipientId), recipientId, UUID.randomUUID().toString(), "PLAIN_TEXT", Base64.getEncoder().encodeToString(text.getBytes()), null, null )); @@ -286,72 +129,6 @@ private static void sendTextMessage(HttpClient client, String recipientId, Strin // } } - private static void transactions(HttpClient client, List receivers, byte[] aseKey, String pin) throws IOException { - MixinResponse transactionResponse = client.getAssetService().transactionsCall( - new TransactionRequest(Sample.CNB_assetId, new OpponentMultisig( - receivers, - 2 - ), null, Sample.amount, encryptPin(aseKey, pin) - , null, null)).execute().body(); - assert transactionResponse != null; - if (transactionResponse.isSuccess()) { - System.out.printf("TransactionsResponse success: %s%n", Objects.requireNonNull(transactionResponse.getData()).getSnapshotId()); - } else { - System.out.printf("Transactions failure: %s", Objects.requireNonNull(transactionResponse.getError()).getDescription()); - } - } - - private static void transactionsOpponentKey(HttpClient client, String opponentKey, byte[] aseKey, String pin) throws IOException { - MixinResponse transactionResponse = client.getAssetService().transactionsCall( - new TransactionRequest(Sample.CNB_assetId, null, opponentKey, Sample.amount, encryptPin(aseKey, pin) - , null, null)).execute().body(); - assert transactionResponse != null; - if (transactionResponse.isSuccess()) { - System.out.printf("TransactionsResponse success: %s%n", Objects.requireNonNull(transactionResponse.getData()).getTransactionHash()); - } else { - System.out.printf("Transactions failure: %s", Objects.requireNonNull(transactionResponse.getError()).getDescription()); - } - } - - - private static void networkSnapshot(HttpClient client, String snapshotId) throws IOException { - MixinResponse snapshotResponse = client.getSnapshotService().networkSnapshotCall(snapshotId).execute().body(); - assert snapshotResponse != null; - if (snapshotResponse.isSuccess()) { - System.out.printf("Success: %s%n", Objects.requireNonNull(snapshotResponse.getData()).getSnapshotId()); - } else { - System.out.printf("failure: %s", Objects.requireNonNull(snapshotResponse.getError()).getDescription()); - } - } - - private static void networkSnapshots(HttpClient client, String assetId, String offset, int limit, String order) throws IOException { - MixinResponse> snapshotResponse = client.getSnapshotService().networkSnapshotsCall(assetId, offset, limit, order).execute().body(); - assert snapshotResponse != null; - if (snapshotResponse.isSuccess()) { - List data = snapshotResponse.getData(); - System.out.printf("Success: %d%n", Objects.requireNonNull(snapshotResponse.getData()).size()); - for (NetworkSnapshot datum : data) { - System.out.println(datum.toString()); - } - } else { - System.out.printf("failure: %s", Objects.requireNonNull(snapshotResponse.getError()).getDescription()); - } - } - - private static void readGhostKey(HttpClient client) throws IOException { - List userList = new ArrayList<>(); - userList.add("639ec50a-d4f1-4135-8624-3c71189dcdcc"); - userList.add("d3bee23a-81d4-462e-902a-22dae9ef89ff"); - GhostKeyRequest request = new GhostKeyRequest(userList, 0, ""); - MixinResponse response = client.getUserService().readGhostKeysCall(request).execute().body(); - assert response != null; - - if (response.isSuccess()) { - System.out.printf("ReadGhostKey success %s%n", response.getData()); - } else { - System.out.println("ReadGhostKey failure"); - } - } private static void createConversationAndSendMessage(HttpClient client, String botUserId) throws IOException { List list = new ArrayList<>(); diff --git a/samples/src/main/java/jvmMain/java/safe/TransactionToUser.java b/samples/src/main/java/jvmMain/java/safe/TransactionToUser.java new file mode 100644 index 0000000..22ef005 --- /dev/null +++ b/samples/src/main/java/jvmMain/java/safe/TransactionToUser.java @@ -0,0 +1,32 @@ +package jvmMain.java.safe; + +import jvmMain.java.Common; +import one.mixin.bot.safe.TransactionKt; +import one.mixin.bot.vo.safe.TransactionResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class TransactionToUser { + public static void main(String[] args) { + String traceId = UUID.randomUUID().toString(); + try { + ArrayList receivers = new ArrayList<>(); + receivers.add("cfb018b0-eaf7-40ec-9e07-28a5158f1269"); + List transactions = TransactionKt.sendTransactionToUser( + Common.botClient, + Common.Token.CNB.getAssetId(), + receivers, + "0.0010", + "test from java", + traceId + ); + for (TransactionResponse transaction : transactions) { + System.out.println("view in view transaction in: https://viewblock.io/mixin/tx/" + transaction.getTransactionHash()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/samples/src/main/java/jvmMain/java/safe/Withdrawal.java b/samples/src/main/java/jvmMain/java/safe/Withdrawal.java new file mode 100644 index 0000000..6941b54 --- /dev/null +++ b/samples/src/main/java/jvmMain/java/safe/Withdrawal.java @@ -0,0 +1,34 @@ +package jvmMain.java.safe; + +import jvmMain.java.Common; +import jvmMain.kotlin.Config; +import one.mixin.bot.safe.SafeException; +import one.mixin.bot.safe.TransactionKt; +import one.mixin.bot.vo.safe.TransactionResponse; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +public class Withdrawal { + public static void main(String[] args) { + String traceId = UUID.randomUUID().toString(); + try { + List transactions = TransactionKt.withdrawalToAddress( + Common.botClient, + Common.Token.TRON_USDT.getAssetId(), + "TQ5NMqJjhpQGK7YJbESKfjXZoQXf2hZL7r", + null, + "0.0001", + null, + traceId + ); + for (TransactionResponse transaction : transactions) { + System.out.println("view in view transaction in: https://viewblock.io/mixin/tx/" + transaction.getTransactionHash()); + } + } catch (SafeException | IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/samples/src/main/java/jvmMain/kotlin/BlazeSample.kt b/samples/src/main/java/jvmMain/kotlin/BlazeSample.kt index 23ef57c..77e28b0 100644 --- a/samples/src/main/java/jvmMain/kotlin/BlazeSample.kt +++ b/samples/src/main/java/jvmMain/kotlin/BlazeSample.kt @@ -8,25 +8,20 @@ import one.mixin.bot.blaze.BlazeHandler import one.mixin.bot.blaze.BlazeMsg import one.mixin.bot.blaze.sendTextMsg import one.mixin.bot.extension.base64Decode +import one.mixin.bot.extension.hexStringToByteArray import one.mixin.bot.util.newKeyPairFromPrivateKey +import one.mixin.bot.util.newKeyPairFromSeed -fun main(): Unit = - runBlocking { - val job = - launch { - val keyPair = newKeyPairFromPrivateKey(Config.privateKey.base64Decode()) - val blazeClient = - BlazeClient.Builder() - .configSafeUser(Config.userId, Config.sessionId, keyPair.privateKey) - .enableDebug() - .enableParseData() - .enableAutoAck() - .blazeHandler(MyBlazeHandler()) - .build() - blazeClient.start() - } - job.join() +fun main(): Unit = runBlocking { + val job = launch { + val keyPair = newKeyPairFromSeed(Config.BOT_SESSION_PRIVATE_KEY.hexStringToByteArray()) + val blazeClient = + BlazeClient.Builder().configSafeUser(Config.BOT_USER_ID, Config.BOT_SESSION_ID, keyPair.privateKey) + .enableDebug().enableParseData().enableAutoAck().blazeHandler(MyBlazeHandler()).build() + blazeClient.start() } + job.join() +} private class MyBlazeHandler : BlazeHandler { override fun onMessage( diff --git a/samples/src/main/java/jvmMain/kotlin/Common.kt b/samples/src/main/java/jvmMain/kotlin/Common.kt new file mode 100644 index 0000000..c66941a --- /dev/null +++ b/samples/src/main/java/jvmMain/kotlin/Common.kt @@ -0,0 +1,21 @@ +package jvmMain.kotlin + +import one.mixin.bot.HttpClient +import one.mixin.bot.api.MixinResponse +import one.mixin.bot.extension.hexStringToByteArray + +val botClient = HttpClient.Builder().configSafeUser( + userId = Config.BOT_USER_ID, + sessionId = Config.BOT_SESSION_ID, + sessionPrivateKey = Config.BOT_SESSION_PRIVATE_KEY.hexStringToByteArray(), + spendPrivateKey = Config.BOT_SPEND_KEY.hexStringToByteArray(), +).enableDebug().build() + + +enum class Token(val assetId: String) { + CNB("965e5c6e-434c-3fa9-b780-c50f43cd955c"), + + TRON_USDT("b91e18ff-a9ae-3dc7-8679-e935d9a4b34b"), + + TRX("25dabac5-056a-48ff-b9f9-f67395dc407c"), +} diff --git a/samples/src/main/java/jvmMain/kotlin/Config.kt b/samples/src/main/java/jvmMain/kotlin/Config.kt index 201bc23..21d0d84 100644 --- a/samples/src/main/java/jvmMain/kotlin/Config.kt +++ b/samples/src/main/java/jvmMain/kotlin/Config.kt @@ -1,10 +1,8 @@ package jvmMain.kotlin object Config { - const val pin = "967784" - const val userId = "24888245-d200-4907-aff0-1303d94217d5" - const val sessionId = "0d89aa10-f559-400e-9323-9d103f9fd49e" - const val privateKey = - "ZywWUtZxMUxdS6LHSEqVsxFr9Y3pM5OJDx648Hm54Tgn9IoGhUOLbNoQ2nKewVNnCsBF3qOcb4nuiowuxQax4w" - const val pinTokenPem = "kMu0ihumvCC0t1sgbD1n1sv8TQLFVje4qWv-Wpy_Ihg" + const val BOT_USER_ID = "" + const val BOT_SESSION_ID = "" + const val BOT_SESSION_PRIVATE_KEY = "" + const val BOT_SPEND_KEY = "" } diff --git a/samples/src/main/java/jvmMain/kotlin/SafeSample.kt b/samples/src/main/java/jvmMain/kotlin/SafeSample.kt index d101a61..9d239a1 100644 --- a/samples/src/main/java/jvmMain/kotlin/SafeSample.kt +++ b/samples/src/main/java/jvmMain/kotlin/SafeSample.kt @@ -1,185 +1,191 @@ package jvmMain.kotlin - -import kotlinx.coroutines.runBlocking -import one.mixin.bot.HttpClient -import one.mixin.bot.encryptPin -import one.mixin.bot.encryptTipPin -import one.mixin.bot.extension.assetIdToAsset -import one.mixin.bot.extension.base64Decode -import one.mixin.bot.extension.base64Encode -import one.mixin.bot.extension.base64UrlDecode -import one.mixin.bot.extension.base64UrlEncode -import one.mixin.bot.extension.hexStringToByteArray -import one.mixin.bot.extension.nowInUtcNano -import one.mixin.bot.extension.toHex -import one.mixin.bot.safe.SafeException -import one.mixin.bot.safe.TipBody -import one.mixin.bot.safe.registerSafe -import one.mixin.bot.safe.sendTransaction -import one.mixin.bot.safe.updateTipPin -import one.mixin.bot.util.decryptPinToken -import one.mixin.bot.util.generateEd25519KeyPair -import one.mixin.bot.util.generateRandomBytes -import one.mixin.bot.util.newKeyPairFromSeed -import one.mixin.bot.util.toBeByteArray -import one.mixin.bot.vo.Account -import one.mixin.bot.vo.PinRequest -import one.mixin.bot.vo.safe.MixAddress -import one.mixin.bot.vo.safe.TransactionRecipient -import one.mixin.bot.vo.safe.toMixAddress -import java.nio.file.Path -import java.nio.file.Paths -import java.util.UUID - -fun main() = - runBlocking { - val botClient = - HttpClient.Builder().useCNServer().configSafeUser( - Config.userId, - Config.sessionId, - Config.privateKey.base64UrlDecode(), - Config.pinTokenPem.base64UrlDecode(), - Config.pin.hexStringToByteArray(), - ).enableDebug().build() - - updateFromLegacyPin(botClient) - - // val user = createTipPin(botClient) ?: return@runBlocking - - // use Transaction.kt or MixAddress.kt should load libgojni.so first - val currentRelativePath: Path = Paths.get("") - val s: String = currentRelativePath.toAbsolutePath().toString() - println("Current absolute path is: $s") - System.load("$s/library/libs/darwin/amd64/libgojni.so") - - // transactionToOne(botClient) - - // transactionToMultiple(botClient) - } - -private suspend fun updateFromLegacyPin(botClient: HttpClient) { - // create user - val sessionKey = generateEd25519KeyPair() - val sessionSecret = sessionKey.publicKey.base64Encode() - val user = createUser(botClient, sessionSecret) ?: return - - val userClient = - HttpClient.Builder().useCNServer().configSafeUser( - user.userId, - user.sessionId, - sessionKey.privateKey, - ).enableDebug().build() - - // decrypt pin token - val userPrivateKey = sessionKey.privateKey - val userAesKey = decryptPinToken(user.pinToken.base64Decode(), userPrivateKey) - - // create user pin - var response = userClient.userService.createPin(PinRequest(encryptPin(userAesKey, DEFAULT_PIN.toByteArray()))) - if (response.isSuccess()) { - println("Create pin success ${response.data}") - } else { - throw Exception("Create pin failure ${response.error}") - } - // verify usr pin - response = userClient.userService.pinVerify(PinRequest(encryptPin(userAesKey, DEFAULT_PIN.toByteArray()))) - if (response.isSuccess()) { - println("Verify pin success") - } else { - throw Exception("Verify pin failure ${response.error}") - } - - // update tip pin - val tipSeed = generateRandomBytes(32) - val keyPair = newKeyPairFromSeed(tipSeed) - updateTipPin(userClient, keyPair.publicKey.toHex(), userPrivateKey.base64UrlEncode(), user.pinToken, DEFAULT_PIN) - // verify tip pin - val timestamp = nowInUtcNano() - response = userClient.userService.pinVerify(PinRequest(encryptTipPin(userAesKey, TipBody.forVerify(timestamp), keyPair.privateKey), timestamp = timestamp)) - if (response.isSuccess()) { - println("Verify tip pin success") - } else { - throw Exception("Verify tip pin failure ${response.error}") - } - - // register safe - registerSafe( - userClient, - user.userId, - keyPair.privateKey.toHex(), - keyPair.privateKey.toHex(), - userPrivateKey.base64UrlEncode(), - user.pinToken, - ) -} - -private suspend fun createTipPin(botClient: HttpClient): Account? { - // create user - val sessionKey = generateEd25519KeyPair() - val sessionSecret = sessionKey.publicKey.base64Encode() - val user = createUser(botClient, sessionSecret) ?: return null - - val userClient = - HttpClient.Builder().useCNServer().configSafeUser( - user.userId, - user.sessionId, - sessionKey.privateKey, - ).enableDebug().build() - - // decrypt pin token - val userPrivateKey = sessionKey.privateKey - val userAesKey = decryptPinToken(user.pinToken.base64Decode(), userPrivateKey) - - // create user tip pin - val tipSeed = generateRandomBytes(32) - val keyPair = newKeyPairFromSeed(tipSeed) - val response = - userClient.userService.createPin( - PinRequest(encryptPin(userAesKey, keyPair.publicKey + 1L.toBeByteArray())), - ) - if (response.isSuccess()) { - println("Create tip pin success ${response.data?.userId}") - } else { - println("Create tip pin failure ${response.error}") - } - - // register safe - return registerSafe( - userClient, - user.userId, - keyPair.privateKey.toHex(), - keyPair.privateKey.toHex(), - userPrivateKey.base64UrlEncode(), - user.pinToken, - ) -} - -fun transactionToOne(botClient: HttpClient) { - val asset = assetIdToAsset("965e5c6e-434c-3fa9-b780-c50f43cd955c") // cnb - val mixAddress = - MixAddress.newUuidMixAddress(listOf("d3bee23a-81d4-462e-902a-22dae9ef89ff"), 1) - ?: throw SafeException("newUuidMixAddress got null mixAddress") - val transactionRecipient = TransactionRecipient(mixAddress, "0.013") - val trace = UUID.randomUUID().toString() - println("trace: $trace") - val tx = sendTransaction(botClient, asset, transactionRecipient, trace, "") - println(tx) - - try { - sendTransaction(botClient, asset, transactionRecipient, trace, "") - } catch (e: SafeException) { - println("use same id should throw exception ${e.stackTraceToString()}") - } -} - -fun transactionToMultiple(botClient: HttpClient) { - val asset = "965e5c6e-434c-3fa9-b780-c50f43cd955c" // cnb - val mixAddress = - "MIXDLSoouhdcvedoiSzNHNRR4FNqVNwwgHUXkFoApTsz35fBHSNGyZEqGCzWuwDYrrWDwCXiaNcPec4C5cW8tCiE7BUHvs6A9YZ4B6FiFAEYY5Nd1etLA7aE7".toMixAddress() - ?: throw SafeException("toMixAddress got null mixAddress") - val transactionRecipient = TransactionRecipient(mixAddress, "0.012") - val trace = UUID.randomUUID().toString() - println("trace: $trace") - val tx = sendTransaction(botClient, asset, transactionRecipient, trace, "") - println(tx) -} +// +//import kotlinx.coroutines.runBlocking +//import one.mixin.bot.HttpClient +//import one.mixin.bot.encryptPin +//import one.mixin.bot.encryptTipPin +//import one.mixin.bot.extension.assetIdToAsset +//import one.mixin.bot.extension.base64Decode +//import one.mixin.bot.extension.base64Encode +//import one.mixin.bot.extension.base64UrlDecode +//import one.mixin.bot.extension.base64UrlEncode +//import one.mixin.bot.extension.hexStringToByteArray +//import one.mixin.bot.extension.nowInUtcNano +//import one.mixin.bot.extension.toHex +//import one.mixin.bot.safe.SafeException +//import one.mixin.bot.safe.TipBody +//import one.mixin.bot.safe.registerSafe +//import one.mixin.bot.safe.sendTransaction +//import one.mixin.bot.safe.updateTipPin +//import one.mixin.bot.util.decryptPinToken +//import one.mixin.bot.util.generateEd25519KeyPair +//import one.mixin.bot.util.generateRandomBytes +//import one.mixin.bot.util.newKeyPairFromSeed +//import one.mixin.bot.util.toBeByteArray +//import one.mixin.bot.vo.Account +//import one.mixin.bot.vo.PinRequest +//import one.mixin.bot.vo.safe.MixAddress +//import one.mixin.bot.vo.safe.TransactionRecipient +//import one.mixin.bot.vo.safe.toMixAddress +//import java.nio.file.Path +//import java.nio.file.Paths +//import java.util.UUID +// +//fun main() = runBlocking { +// val key = +// newKeyPairFromSeed("706978a40ff2b66168da563eaee560881530ca1450b61ea31753d85ab04e5683".hexStringToByteArray()) +// val botClient = HttpClient.Builder().useCNServer().configSafeUser( +// "489cfe0b-08d8-47f4-a330-fff193cc8086", +// "bb860112-d9de-4a86-b9fd-e0512bdf7c92", +// (key.privateKey + key.publicKey), +// Config.pinTokenPem.base64UrlDecode(), +// Config.pin.hexStringToByteArray(), +// ).enableDebug().build() +// +// updateFromLegacyPin(botClient) +// +// // val user = createTipPin(botClient) ?: return@runBlocking +// +// // use Transaction.kt or MixAddress.kt should load libgojni.so first +// val currentRelativePath: Path = Paths.get("") +// val s: String = currentRelativePath.toAbsolutePath().toString() +// println("Current absolute path is: $s") +// System.load("$s/library/libs/darwin/amd64/libgojni.so") +// +// // transactionToOne(botClient) +// +// // transactionToMultiple(botClient) +//} +// +//private suspend fun updateFromLegacyPin(botClient: HttpClient) { // create user +// val sessionKey = generateEd25519KeyPair() +// val sessionSecret = sessionKey.publicKey.base64Encode() +// val user = createUser(botClient, sessionSecret) ?: return +// +// val userClient = HttpClient.Builder().useCNServer().configSafeUser( +// user.userId, +// user.sessionId, +// sessionKey.privateKey, +// ).enableDebug().build() +// +// // decrypt pin token +// val userPrivateKey = sessionKey.privateKey +// val userAesKey = decryptPinToken(user.pinToken.base64Decode(), userPrivateKey) +// +// // create user pin +// var response = userClient.userService.createPin(PinRequest(encryptPin(userAesKey, DEFAULT_PIN.toByteArray()))) +// if (response.isSuccess()) { +// println("Create pin success ${response.data}") +// } else { +// throw Exception("Create pin failure ${response.error}") +// } // verify usr pin +// response = userClient.userService.pinVerify(PinRequest(encryptPin(userAesKey, DEFAULT_PIN.toByteArray()))) +// if (response.isSuccess()) { +// println("Verify pin success") +// } else { +// throw Exception("Verify pin failure ${response.error}") +// } +// +// // update tip pin +// val tipSeed = generateRandomBytes(32) +// val keyPair = newKeyPairFromSeed(tipSeed) +// updateTipPin( +// userClient, +// keyPair.publicKey.toHex(), +// userPrivateKey.base64UrlEncode(), +// user.pinToken, +// DEFAULT_PIN +// ) // verify tip pin +// val timestamp = nowInUtcNano() +// response = userClient.userService.pinVerify( +// PinRequest( +// encryptTipPin( +// userAesKey, +// TipBody.forVerify(timestamp), +// keyPair.privateKey +// ), timestamp = timestamp +// ) +// ) +// if (response.isSuccess()) { +// println("Verify tip pin success") +// } else { +// throw Exception("Verify tip pin failure ${response.error}") +// } +// +// // register safe +// registerSafe( +// userClient, +// user.userId, +// keyPair.privateKey.toHex(), +// keyPair.privateKey.toHex(), +// userPrivateKey.base64UrlEncode(), +// user.pinToken, +// ) +//} +// +//private suspend fun createTipPin(botClient: HttpClient): Account? { // create user +// val sessionKey = generateEd25519KeyPair() +// val sessionSecret = sessionKey.publicKey.base64Encode() +// val user = createUser(botClient, sessionSecret) ?: return null +// +// val userClient = HttpClient.Builder().useCNServer().configSafeUser( +// user.userId, +// user.sessionId, +// sessionKey.privateKey, +// ).enableDebug().build() +// +// // decrypt pin token +// val userPrivateKey = sessionKey.privateKey +// val userAesKey = decryptPinToken(user.pinToken.base64Decode(), userPrivateKey) +// +// // create user tip pin +// val tipSeed = generateRandomBytes(32) +// val keyPair = newKeyPairFromSeed(tipSeed) +// val response = userClient.userService.createPin( +// PinRequest(encryptPin(userAesKey, keyPair.publicKey + 1L.toBeByteArray())), +// ) +// if (response.isSuccess()) { +// println("Create tip pin success ${response.data?.userId}") +// } else { +// println("Create tip pin failure ${response.error}") +// } +// +// // register safe +// return registerSafe( +// userClient, +// user.userId, +// keyPair.privateKey.toHex(), +// keyPair.privateKey.toHex(), +// userPrivateKey.base64UrlEncode(), +// user.pinToken, +// ) +//} +// +//fun transactionToOne(botClient: HttpClient) { +// val asset = assetIdToAsset("965e5c6e-434c-3fa9-b780-c50f43cd955c") // cnb +// val mixAddress = MixAddress.newUuidMixAddress(listOf("d3bee23a-81d4-462e-902a-22dae9ef89ff"), 1) +// ?: throw SafeException("newUuidMixAddress got null mixAddress") +// val transactionRecipient = TransactionRecipient(mixAddress, "0.013") +// val trace = UUID.randomUUID().toString() +// println("trace: $trace") +// val tx = sendTransaction(botClient, asset, transactionRecipient, trace, "") +// println(tx) +// +// try { +// sendTransaction(botClient, asset, transactionRecipient, trace, "") +// } catch (e: SafeException) { +// println("use same id should throw exception ${e.stackTraceToString()}") +// } +//} +// +//fun transactionToMultiple(botClient: HttpClient) { +// val asset = "965e5c6e-434c-3fa9-b780-c50f43cd955c" // cnb +// val mixAddress = +// "MIXDLSoouhdcvedoiSzNHNRR4FNqVNwwgHUXkFoApTsz35fBHSNGyZEqGCzWuwDYrrWDwCXiaNcPec4C5cW8tCiE7BUHvs6A9YZ4B6FiFAEYY5Nd1etLA7aE7".toMixAddress() +// ?: throw SafeException("toMixAddress got null mixAddress") +// val transactionRecipient = TransactionRecipient(mixAddress, "0.012") +// val trace = UUID.randomUUID().toString() +// println("trace: $trace") +// val tx = sendTransaction(botClient, asset, transactionRecipient, trace, "") +// println(tx) +//} diff --git a/samples/src/main/java/jvmMain/kotlin/Sample.kt b/samples/src/main/java/jvmMain/kotlin/Sample.kt index fce191a..1184cb5 100644 --- a/samples/src/main/java/jvmMain/kotlin/Sample.kt +++ b/samples/src/main/java/jvmMain/kotlin/Sample.kt @@ -1,128 +1,64 @@ package jvmMain.kotlin -import jvmMain.kotlin.Config.pin import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import one.mixin.bot.HttpClient -import one.mixin.bot.api.SnapshotService import one.mixin.bot.encryptPin import one.mixin.bot.extension.base64Decode import one.mixin.bot.extension.base64Encode -import one.mixin.bot.extension.base64UrlDecode -import one.mixin.bot.extension.hexStringToByteArray import one.mixin.bot.util.decryptPinToken import one.mixin.bot.util.generateEd25519KeyPair -import one.mixin.bot.util.newKeyPairFromPrivateKey -import one.mixin.bot.util.newKeyPairFromSeed -import one.mixin.bot.util.toBeByteArray import one.mixin.bot.vo.AccountRequest -import one.mixin.bot.vo.AddressRequest import one.mixin.bot.vo.ConversationRequest -import one.mixin.bot.vo.GhostKeyRequest import one.mixin.bot.vo.MessageRequest -import one.mixin.bot.vo.NetworkSnapshot import one.mixin.bot.vo.ParticipantRequest import one.mixin.bot.vo.PinRequest -import one.mixin.bot.vo.Snapshot -import one.mixin.bot.vo.TransactionRequest -import one.mixin.bot.vo.TransferRequest import one.mixin.bot.vo.User -import one.mixin.bot.vo.WithdrawalRequest import one.mixin.bot.vo.generateTextMessageRequest import java.util.Random import java.util.UUID -const val CNB_ID = "965e5c6e-434c-3fa9-b780-c50f43cd955c" -const val BTC_ID = "c6d0c728-2624-429b-8e0d-d9d19b6592fa" const val DEFAULT_PIN = "131416" -const val DEFAULT_TIP_PIN = "5011c07b101e07b74667398d57a40e9001aa8f6c13fe0836a07a1b5f7cf71e4e" -const val DEFAULT_AMOUNT = "0.01" +fun main() = runBlocking { + // create user + val sessionKey = generateEd25519KeyPair() + val sessionSecret = sessionKey.publicKey.base64Encode() + val user = createUser(botClient, sessionSecret) + user ?: return@runBlocking + val userClient = HttpClient.Builder().useCNServer().configSafeUser( + user.userId, + user.sessionId, + sessionKey.privateKey, + ).enableDebug().build() + userClient.userService.getMe() -@Deprecated( - message = "Deprecated sample for Mixin legacy network", - replaceWith = ReplaceWith("@{link SafeSample}", "jvmMain.kotlin.SafeSample"), -) -fun main() = - runBlocking { - val key = newKeyPairFromPrivateKey(Config.privateKey.base64UrlDecode()) - val pinToken = decryptPinToken(Config.pinTokenPem.base64UrlDecode(), key.privateKey) - val botClient = - HttpClient.Builder().useCNServer().configSafeUser( - Config.userId, - Config.sessionId, - key.privateKey, - ).enableDebug().build() + // decrypt pin token + val userPrivateKey = sessionKey.privateKey + val userAesKey = decryptPinToken(user.pinToken.base64Decode(), userPrivateKey) - // create user - val sessionKey = generateEd25519KeyPair() - val sessionSecret = sessionKey.publicKey.base64Encode() - val user = createUser(botClient, sessionSecret) - user ?: return@runBlocking - val userClient = - HttpClient.Builder().useCNServer().configSafeUser( - user.userId, - user.sessionId, - sessionKey.privateKey, - ).enableDebug().build() - userClient.userService.getMe() + // create user's pin + createPin(userClient, userAesKey) - // decrypt pin token - val userPrivateKey = sessionKey.privateKey - val userAesKey = decryptPinToken(user.pinToken.base64Decode(), userPrivateKey) + delay(2000) - // create user's pin - createPin(userClient, userAesKey) + // Send text message + sendTextMessage(botClient, "639ec50a-d4f1-4135-8624-3c71189dcdcc", "Text message") - // bot transfer to user - transferToUser(botClient, user.userId, pinToken, pin) + createConversationAndSendMessage(botClient, Config.BOT_USER_ID) - delay(2000) - - // Get ticker - getTicker(userClient) - - // Get fiats - getFiats(userClient) - - // Get BTC fee - getFee(userClient) - - // Get asset - getAsset(userClient) - - // Create address - val addressId = createAddress(userClient, userAesKey) - if (addressId != null) { - // Withdrawal - withdrawalToAddress(userClient, addressId, userAesKey) - } - - // Send text message - sendTextMessage(botClient, "639ec50a-d4f1-4135-8624-3c71189dcdcc", "Text message") - - createConversationAndSendMessage(botClient, Config.userId) - - // Transactions - transactions(botClient, pinToken) - - networkSnapshots(botClient, CNB_ID) - networkSnapshot(botClient, "c8e73a02-b543-4100-bd7a-879ed4accdfc") - - readGhostKey(botClient) - return@runBlocking - } + return@runBlocking +} internal suspend fun createUser( client: HttpClient, sessionSecret: String, ): User? { - val response = - client.userService.createUsers( - AccountRequest( - Random().nextInt(10).toString() + "User", - sessionSecret, - ), - ) + val response = client.userService.createUsers( + AccountRequest( + Random().nextInt(10).toString() + "User", + sessionSecret, + ), + ) return response.data } @@ -130,26 +66,9 @@ internal suspend fun createPin( client: HttpClient, userAesKey: ByteArray, ) { - val response = - client.userService.createPin( - PinRequest(encryptPin(userAesKey, DEFAULT_PIN.toByteArray())), - ) - if (response.isSuccess()) { - println("Create pin success ${response.data?.userId}") - } else { - println("Create pin failure ${response.error}") - } -} - -internal suspend fun createTipPin( - client: HttpClient, - userAesKey: ByteArray, -) { - val keyPair = newKeyPairFromSeed(DEFAULT_TIP_PIN.hexStringToByteArray()) - val response = - client.userService.createPin( - PinRequest(encryptPin(userAesKey, keyPair.publicKey + 1L.toBeByteArray())), - ) + val response = client.userService.createPin( + PinRequest(encryptPin(userAesKey, DEFAULT_PIN.toByteArray())), + ) if (response.isSuccess()) { println("Create pin success ${response.data?.userId}") } else { @@ -157,142 +76,21 @@ internal suspend fun createTipPin( } } -internal suspend fun transferToUser( - client: HttpClient, - userId: String, - aseKey: ByteArray, - pin: String, -): Snapshot? { - val response = - client.snapshotService.transfer( - TransferRequest( - CNB_ID, - userId, - DEFAULT_AMOUNT, - encryptPin(aseKey, pin.toByteArray()), - ), - ) - var snapshot: Snapshot? = null - if (response.isSuccess()) { - snapshot = response.data - println("Transfer success: ${response.data?.snapshotId}") - } else { - println("Transfer failure ${response.error}") - } - return snapshot -} - -private suspend fun getAsset(client: HttpClient) { - // Get asset - val assetResponse = client.assetService.getAsset(BTC_ID) - if (assetResponse.isSuccess()) { - println( - "Assets ${assetResponse.data?.symbol}: ${assetResponse.data?.balance} ${assetResponse.data?.depositEntries?.last()?.properties}", - ) - } else { - println("Assets failure ${assetResponse.error}") - } -} - -private suspend fun getFiats(client: HttpClient) { - // Get fiats - val fiatsResponse = client.assetService.getFiats() - if (fiatsResponse.isSuccess()) { - println("Fiats ${fiatsResponse.data?.get(0)?.code}: ${fiatsResponse.data?.get(0)?.rate}") - } else { - println("Fiats failure ${fiatsResponse.error}") - } -} - -private suspend fun getFee(client: HttpClient) { - // Get fee - val feeResponse = client.assetService.assetsFee(BTC_ID) - if (feeResponse.isSuccess()) { - println("Fee ${feeResponse.data?.amount}") - } else { - println("Fee failure ${feeResponse.error}") - } -} - -private suspend fun getTicker(client: HttpClient) { - // Get fee - val tickerResponse = client.assetService.ticker(BTC_ID) - if (tickerResponse.isSuccess()) { - println("Ticker ${tickerResponse.data}") - } else { - println("Ticker failure ${tickerResponse.error}") - } -} - -private suspend fun createAddress( - client: HttpClient, - userAesKey: ByteArray, -): String? { - // Create address - val addressesResponse = - client.addressService.createAddresses( - AddressRequest( - CNB_ID, - "0x45315C1Fd776AF95898C77829f027AFc578f9C2B", - null, - "label", - encryptPin( - userAesKey, - DEFAULT_PIN, - ), - ), - ) - - if (addressesResponse.isSuccess()) { - println("Create address ${addressesResponse.data?.addressId}") - } else { - println("Create address failure ${addressesResponse.error}") - } - return addressesResponse.data?.addressId -} - -private suspend fun withdrawalToAddress( - client: HttpClient, - addressId: String, - userAesKey: ByteArray, -) { - // Withdrawals - val withdrawalsResponse = - client.snapshotService.withdrawals( - WithdrawalRequest( - addressId, - DEFAULT_AMOUNT, - encryptPin( - userAesKey, - DEFAULT_PIN, - ), - UUID.randomUUID().toString(), - "withdrawal test", - ), - ) - if (withdrawalsResponse.isSuccess()) { - println("Withdrawal success: ${withdrawalsResponse.data?.snapshotId}") - } else { - println("Withdrawal failure ${withdrawalsResponse.error}") - } -} - private suspend fun sendTextMessage( client: HttpClient, recipientId: String, text: String, ) { - val response = - client.messageService.postMessage( - listOf( - generateTextMessageRequest( - Config.userId, - recipientId, - UUID.randomUUID().toString(), - text, - ), + val response = client.messageService.postMessage( + listOf( + generateTextMessageRequest( + Config.BOT_USER_ID, + recipientId, + UUID.randomUUID().toString(), + text, ), - ) + ), + ) if (response.isSuccess()) { println("Send success") } else { @@ -300,106 +98,25 @@ private suspend fun sendTextMessage( } } -private suspend fun transactions( - client: HttpClient, - userAesKey: ByteArray, -) { - // Transactions - val transactionsResponse = - client.assetService.transactions( - TransactionRequest( - CNB_ID, - // OpponentMultisig(listOf("00c5a4ae-dcdc-48db-ab8e-a7eef69b441d", "087e91ff-7169-451a-aaaa-5b3297411a4b", "4e0e6e6b-6c9d-4e99-b7f1-1356322abec3"), 2), - opponentMultisig = null, - opponentKey = "XINQTmRReDuPEUAVEyDyE2mBgxa1ojVRAvpYcKs5nSA7FDBBfAEeVRn8s9vAm3Cn1qzQ7JtjG62go4jSJU6yWyRUKHpamWAM", // test address - DEFAULT_AMOUNT, - encryptPin( - userAesKey, - pin, - ), - UUID.randomUUID().toString(), - "memo", - ), - ) - if (transactionsResponse.isSuccess()) { - println("Transactions success: ${transactionsResponse.data?.snapshotId}") - } else { - println("Transactions failure ${transactionsResponse.error}") - } -} - -internal suspend fun networkSnapshot( - client: HttpClient, - snapshotId: String, -) { - val snapshotResponse = client.snapshotService.networkSnapshot(snapshotId) - if (snapshotResponse.isSuccess()) { - println("Network snapshot success: ${snapshotResponse.data?.snapshotId}") - } else { - println("Network snapshot failure ${snapshotResponse.error}") - } -} - -internal suspend fun networkSnapshots( - client: HttpClient, - assetId: String, - offset: String? = null, - limit: Int = SnapshotService.LIMIT, - order: String? = null, -): List? { - val snapshotResponse = client.snapshotService.networkSnapshots(assetId, offset, limit, order) - var networkSnapshots: List? = null - if (snapshotResponse.isSuccess()) { - networkSnapshots = snapshotResponse.data as List - println("Network snapshots success") - for (s in networkSnapshots) { - println(s) - } - } else { - println("Network snapshot failure: ${snapshotResponse.error?.description}") - } - return networkSnapshots -} - -private suspend fun readGhostKey(client: HttpClient) { - val request = - GhostKeyRequest( - listOf( - "639ec50a-d4f1-4135-8624-3c71189dcdcc", - "d3bee23a-81d4-462e-902a-22dae9ef89ff", - ), - 0, - "", - ) - val response = client.userService.readGhostKeys(request) - if (response.isSuccess()) { - println("ReadGhostKey success ${response.data}") - } else { - println("ReadGhostKey failure ${response.error}") - } -} internal suspend fun createConversationAndSendMessage( client: HttpClient, botUserId: String, ) { - val botParticipant = - ParticipantRequest( - userId = botUserId, - role = "", - ) - val userParticipant = - ParticipantRequest( - userId = "e26808d4-b31f-4e3b-9521-19e529b967b0", - role = "", - ) - val conversationRequest = - ConversationRequest( - conversationId = UUID.randomUUID().toString(), - category = "GROUP", - name = "test group", - participants = listOf(botParticipant, userParticipant), - ) + val botParticipant = ParticipantRequest( + userId = botUserId, + role = "", + ) + val userParticipant = ParticipantRequest( + userId = "e26808d4-b31f-4e3b-9521-19e529b967b0", + role = "", + ) + val conversationRequest = ConversationRequest( + conversationId = UUID.randomUUID().toString(), + category = "GROUP", + name = "test group", + participants = listOf(botParticipant, userParticipant), + ) val conversationResponse = client.conversationService.create(conversationRequest) if (conversationResponse.isSuccess()) { println("create conversation success ${conversationResponse.data}") @@ -409,14 +126,13 @@ internal suspend fun createConversationAndSendMessage( } val conversation = conversationResponse.data ?: return - val messageRequest = - MessageRequest( - conversationId = conversation.conversationId, - recipientId = UUID.randomUUID().toString(), - messageId = UUID.randomUUID().toString(), - category = "PLAIN_TEXT", - data = requireNotNull("hello from bot".toByteArray().base64Encode()), - ) + val messageRequest = MessageRequest( + conversationId = conversation.conversationId, + recipientId = UUID.randomUUID().toString(), + messageId = UUID.randomUUID().toString(), + category = "PLAIN_TEXT", + data = requireNotNull("hello from bot".toByteArray().base64Encode()), + ) println("messageRequest: $messageRequest") val messageResponse = client.messageService.postMessage(listOf(messageRequest)) if (messageResponse.isSuccess()) { diff --git a/samples/src/main/java/jvmMain/kotlin/UserTransferSample.kt b/samples/src/main/java/jvmMain/kotlin/UserTransferSample.kt deleted file mode 100644 index 0855234..0000000 --- a/samples/src/main/java/jvmMain/kotlin/UserTransferSample.kt +++ /dev/null @@ -1,87 +0,0 @@ -package jvmMain.kotlin - -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import one.mixin.bot.HttpClient -import one.mixin.bot.extension.base64Decode -import one.mixin.bot.extension.base64Encode -import one.mixin.bot.util.calculateAgreement -import one.mixin.bot.util.decryptPinToken -import one.mixin.bot.util.generateEd25519KeyPair -import one.mixin.bot.util.newKeyPairFromPrivateKey -import one.mixin.bot.util.privateKeyToCurve25519 - -@Deprecated( - message = "Deprecated sample for Mixin legacy network", - replaceWith = ReplaceWith("@{link SafeSample}", "jvmMain.kotlin.SafeSample"), -) -fun main() = - runBlocking { - val keyPair = newKeyPairFromPrivateKey(Config.privateKey.base64Decode()) - val pinToken = decryptPinToken(Config.pinTokenPem.base64Decode(), keyPair.privateKey) - val client = - HttpClient.Builder().useCNServer().configSafeUser(Config.userId, Config.sessionId, keyPair.privateKey).build() - - // create alice keys - val aliceSessionKey = generateEd25519KeyPair() - val aliceSessionSecret = aliceSessionKey.publicKey.base64Encode() - - // create alice - val alice = createUser(client, aliceSessionSecret) - alice ?: return@runBlocking - println("alice: $alice") - val aliceClient = - HttpClient.Builder().useCNServer().configSafeUser(alice.userId, alice.sessionId, aliceSessionKey.privateKey).build() - // decrypt pin token - val aliceAesKey = calculateAgreement(alice.pinToken.base64Decode(), privateKeyToCurve25519(aliceSessionKey.privateKey)) - // create alice's pin - createPin(aliceClient, aliceAesKey) - - // create bob keys - val bobSessionKey = generateEd25519KeyPair() - val bobSessionSecret = bobSessionKey.publicKey.base64Encode() - - // create bob - val bob = createUser(client, bobSessionSecret) - bob ?: return@runBlocking - println("bob: $bob") - val bobClient = - HttpClient.Builder().useCNServer().configSafeUser(bob.userId, bob.sessionId, bobSessionKey.privateKey).build() - // decrypt pin token - val bobAesKey = calculateAgreement(bob.pinToken.base64Decode(), privateKeyToCurve25519(bobSessionKey.privateKey)) - // create bob's pin - createPin(bobClient, bobAesKey) - - // bot transfer to alice - val snapshotBot2Alice = transferToUser(client, alice.userId, pinToken, Config.pin) - - delay(4000) - - if (snapshotBot2Alice != null) { - // alice check transfer - networkSnapshot(aliceClient, snapshotBot2Alice.snapshotId) - - val aliceNetworkSnapshots = networkSnapshots(aliceClient, CNB_ID, limit = 5) - assert(aliceNetworkSnapshots?.find { it.snapshotId == snapshotBot2Alice.snapshotId } != null) - } - - // alice transfer to bob - val snapshotAlice2Bob = transferToUser(aliceClient, bob.userId, aliceAesKey, DEFAULT_PIN) - - delay(4000) - - if (snapshotAlice2Bob != null) { - networkSnapshot(bobClient, snapshotAlice2Bob.snapshotId) - - val bobNetworkSnapshots = networkSnapshots(client, CNB_ID, limit = 5) - assert(bobNetworkSnapshots?.find { it.snapshotId == snapshotAlice2Bob.snapshotId } != null) - } - - val botNetworkSnapshot = networkSnapshots(client, CNB_ID, limit = 10) - if (snapshotBot2Alice != null) { - assert(botNetworkSnapshot?.find { it.snapshotId == snapshotBot2Alice.snapshotId } != null) - } - if (snapshotAlice2Bob != null) { - assert(botNetworkSnapshot?.find { it.snapshotId == snapshotAlice2Bob.snapshotId } != null) - } - } diff --git a/samples/src/main/java/jvmMain/kotlin/safe/TransactionToUser.kt b/samples/src/main/java/jvmMain/kotlin/safe/TransactionToUser.kt new file mode 100644 index 0000000..b76b124 --- /dev/null +++ b/samples/src/main/java/jvmMain/kotlin/safe/TransactionToUser.kt @@ -0,0 +1,21 @@ +package jvmMain.kotlin.safe + +import jvmMain.kotlin.Token +import jvmMain.kotlin.botClient +import one.mixin.bot.safe.sendTransactionToUser +import java.util.UUID + +fun main() { + val traceId = UUID.randomUUID().toString() + val transactions = sendTransactionToUser( + botClient = botClient, + receivers = listOf("cfb018b0-eaf7-40ec-9e07-28a5158f1269"), + assetId = Token.CNB.assetId, + amount = "0.003", + memo = "test-memo-from-kotlin", + traceId = traceId, + ) + for (tx in transactions) { + println("view transaction in: https://viewblock.io/mixin/tx/${tx.transactionHash}") + } +} \ No newline at end of file diff --git a/samples/src/main/java/jvmMain/kotlin/safe/Withdrawal.kt b/samples/src/main/java/jvmMain/kotlin/safe/Withdrawal.kt new file mode 100644 index 0000000..2bbcb21 --- /dev/null +++ b/samples/src/main/java/jvmMain/kotlin/safe/Withdrawal.kt @@ -0,0 +1,22 @@ +package jvmMain.kotlin.safe + +import jvmMain.kotlin.Token +import jvmMain.kotlin.botClient +import one.mixin.bot.safe.withdrawalToAddressBlocking +import java.util.UUID + +fun main() { + val traceId = UUID.randomUUID().toString() + val transactions = withdrawalToAddressBlocking( + botClient = botClient, + assetId = Token.TRON_USDT.assetId, + amount = "0.003", + memo = "test-memo-from-kotlin", + destination = "TAXE98CMomoA28MtNpfxUutCBq2i4bDbRv", + tag = null, + traceId = traceId, + ) + for (tx in transactions) { + println("view transaction in: https://viewblock.io/mixin/tx/${tx.transactionHash}") + } +}