├── .gitmodules ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── kotlin │ │ └── org │ │ └── tokend │ │ └── wallet │ │ ├── xdr │ │ ├── utils │ │ │ ├── XdrOptionalField.kt │ │ │ ├── XdrDiscriminantField.kt │ │ │ ├── XdrDecodable.kt │ │ │ ├── XdrEncodable.kt │ │ │ ├── XdrPrimitives.kt │ │ │ ├── XdrDataOutputStream.kt │ │ │ ├── XdrDataInputStream.kt │ │ │ └── ReflectiveXdrDecoder.kt │ │ ├── op_extensions │ │ │ ├── BindExternalAccountOp.kt │ │ │ ├── CreateFeeOp.kt │ │ │ ├── DeleteFeeOp.kt │ │ │ ├── CancelOfferOp.kt │ │ │ ├── CreateBalanceOp.kt │ │ │ ├── DeleteBalanceOp.kt │ │ │ ├── CreateOfferOp.kt │ │ │ ├── SimplePaymentOp.kt │ │ │ └── SimpleSetFeesOp.kt │ │ ├── XdrByteArrayFixed16.kt │ │ ├── XdrByteArrayFixed32.kt │ │ └── XdrByteArrayFixed4.kt │ │ ├── utils │ │ ├── Hashing.kt │ │ ├── ByteCharArray.kt │ │ ├── SecureCharArrayWriter.kt │ │ ├── Base64.kt │ │ └── SecureBase32.kt │ │ ├── PublicKeyFactory.kt │ │ ├── NetworkParams.kt │ │ ├── TransactionBuilder.kt │ │ ├── Account.kt │ │ ├── Base32Check.kt │ │ └── Transaction.kt └── test │ └── kotlin │ └── org │ └── tokend │ └── wallet_test │ ├── Base32CheckTest.kt │ ├── TransactionTest.kt │ ├── NetworkParams.kt │ ├── XdrModelsTests.kt │ ├── TransactionBuilderTest.kt │ ├── AccountTest.kt │ └── DecodingTest.kt ├── .gitignore ├── generateXDR.sh ├── deploy.gradle ├── README.md ├── gradlew.bat ├── CHANGELOG.md ├── gradlew └── LICENSE.txt /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wallet' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokend/kotlin-wallet/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrOptionalField.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | @Target(AnnotationTarget.VALUE_PARAMETER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class XdrOptionalField -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrDiscriminantField.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | @Target(AnnotationTarget.VALUE_PARAMETER) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class XdrDiscriminantField -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 09 00:28:27 EEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/BindExternalAccountOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.xdr.BindExternalSystemAccountIdOp 4 | 5 | class BindExternalAccountOp( 6 | type: Int 7 | ) : BindExternalSystemAccountIdOp( 8 | type, 9 | BindExternalSystemAccountIdOpExt.EmptyVersion() 10 | ) -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/XdrByteArrayFixed16.kt: -------------------------------------------------------------------------------- 1 | // Automatically generated by xdrgen 2 | // DO NOT EDIT or your changes may be overwritten 3 | 4 | package org.tokend.wallet.xdr 5 | 6 | import org.tokend.wallet.xdr.utils.* 7 | 8 | /// Fixed length byte array 9 | class XdrByteArrayFixed16(byteArray: kotlin.ByteArray): XdrFixedByteArray(byteArray) { 10 | override val size: Int 11 | get() = 16 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/XdrByteArrayFixed32.kt: -------------------------------------------------------------------------------- 1 | // Automatically generated by xdrgen 2 | // DO NOT EDIT or your changes may be overwritten 3 | 4 | package org.tokend.wallet.xdr 5 | 6 | import org.tokend.wallet.xdr.utils.* 7 | 8 | /// Fixed length byte array 9 | class XdrByteArrayFixed32(byteArray: kotlin.ByteArray): XdrFixedByteArray(byteArray) { 10 | override val size: Int 11 | get() = 32 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/XdrByteArrayFixed4.kt: -------------------------------------------------------------------------------- 1 | // Automatically generated by xdrgen 2 | // DO NOT EDIT or your changes may be overwritten 3 | 4 | package org.tokend.wallet.xdr 5 | 6 | import org.tokend.wallet.xdr.utils.* 7 | 8 | /// Fixed length byte array 9 | class XdrByteArrayFixed4(byteArray: kotlin.ByteArray): XdrFixedByteArray(byteArray) { 10 | override val size: Int 11 | get() = 4 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrDecodable.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | import org.tokend.wallet.utils.Base64 4 | import java.io.ByteArrayInputStream 5 | 6 | interface XdrDecodable { 7 | /** 8 | * Decodes object of type [T] from XDR content of the [stream] 9 | */ 10 | fun fromXdr(stream: XdrDataInputStream): T 11 | 12 | /** 13 | * Decodes object of type [T] from Base64-encoded XDR content 14 | */ 15 | fun fromBase64(xdrBase64: String): T { 16 | return fromXdr(XdrDataInputStream(ByteArrayInputStream(Base64.decode(xdrBase64.toByteArray())))) 17 | } 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # File-based project format 5 | *.iws 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # IntelliJ 38 | *.iml 39 | .idea/ 40 | 41 | repoCredentials.gradle 42 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/CreateFeeOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.xdr.FeeType 4 | import org.tokend.wallet.xdr.Int64 5 | import org.tokend.wallet.xdr.Uint64 6 | 7 | class CreateFeeOp 8 | @JvmOverloads 9 | constructor( 10 | type: FeeType, 11 | asset: String, 12 | fixed: Int64, 13 | percent: Int64, 14 | upperBound: Int64, 15 | lowerBound: Int64, 16 | subtype: Int64 = 0L, 17 | accountId: String? = null, 18 | accountRole: Uint64? = null 19 | ) : SimpleSetFeesOp(false, type, asset, fixed, percent, 20 | upperBound, lowerBound, subtype, accountId, accountRole) -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/DeleteFeeOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.xdr.FeeType 4 | import org.tokend.wallet.xdr.Int64 5 | import org.tokend.wallet.xdr.Uint64 6 | 7 | class DeleteFeeOp 8 | @JvmOverloads 9 | constructor( 10 | type: FeeType, 11 | asset: String, 12 | fixed: Int64, 13 | percent: Int64, 14 | upperBound: Int64, 15 | lowerBound: Int64, 16 | subtype: Int64 = 0L, 17 | accountId: String? = null, 18 | accountRole: Uint64? = null 19 | ) : SimpleSetFeesOp(true, type, asset, fixed, percent, 20 | upperBound, lowerBound, subtype, accountId, accountRole) -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/CancelOfferOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.xdr.ManageOfferOp 4 | import org.tokend.wallet.xdr.PublicKey 5 | import org.tokend.wallet.xdr.Uint64 6 | import org.tokend.wallet.xdr.XdrByteArrayFixed32 7 | 8 | class CancelOfferOp : ManageOfferOp { 9 | @JvmOverloads 10 | constructor(offerId: Uint64, isBuy: Boolean, 11 | orderBookId: Uint64 = 0 12 | ) : super(PublicKey.KeyTypeEd25519(XdrByteArrayFixed32(ByteArray(32))), 13 | PublicKey.KeyTypeEd25519(XdrByteArrayFixed32(ByteArray(32))), 14 | isBuy, 0, 0, 0, 15 | offerId, orderBookId, ManageOfferOpExt.EmptyVersion()) 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/utils/Hashing.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.utils 2 | 3 | import java.security.MessageDigest 4 | import java.security.NoSuchAlgorithmException 5 | 6 | /** 7 | * Wraps [MessageDigest] calls for hashing algorithms. 8 | */ 9 | object Hashing { 10 | /** 11 | * @return SHA-256 hash of the given data. 12 | */ 13 | @JvmStatic 14 | fun sha256(data: ByteArray): ByteArray { 15 | try { 16 | val digest = MessageDigest.getInstance("SHA-256") 17 | digest.update(data) 18 | return digest.digest() 19 | } catch (e: NoSuchAlgorithmException) { 20 | throw RuntimeException("SHA-256 not implemented") 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/CreateBalanceOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.PublicKeyFactory 4 | import org.tokend.wallet.xdr.AccountID 5 | import org.tokend.wallet.xdr.AssetCode 6 | import org.tokend.wallet.xdr.ManageBalanceAction 7 | import org.tokend.wallet.xdr.ManageBalanceOp 8 | 9 | class CreateBalanceOp(accountId: AccountID, 10 | assetCode: AssetCode) : ManageBalanceOp(ManageBalanceAction.CREATE_UNIQUE, 11 | accountId, assetCode, ManageBalanceOp.ManageBalanceOpExt.EmptyVersion()) { 12 | constructor(accountId: String, 13 | assetCode: String) : this( PublicKeyFactory.fromAccountId(accountId), assetCode) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/DeleteBalanceOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.PublicKeyFactory 4 | import org.tokend.wallet.xdr.AccountID 5 | import org.tokend.wallet.xdr.AssetCode 6 | import org.tokend.wallet.xdr.ManageBalanceAction 7 | import org.tokend.wallet.xdr.ManageBalanceOp 8 | 9 | class DeleteBalanceOp(accountId: AccountID, 10 | assetCode: AssetCode) : ManageBalanceOp(ManageBalanceAction.DELETE_BALANCE, 11 | accountId, assetCode, ManageBalanceOp.ManageBalanceOpExt.EmptyVersion()) { 12 | constructor(accountId: String, 13 | assetCode: String) : this(PublicKeyFactory.fromAccountId(accountId), assetCode) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrEncodable.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | import org.tokend.wallet.utils.Base64 4 | import java.io.ByteArrayOutputStream 5 | 6 | interface XdrEncodable { 7 | /** 8 | * Encodes object to xdr and writes it to specified XdrDataOutputStream 9 | */ 10 | fun toXdr(stream: XdrDataOutputStream) 11 | 12 | /** 13 | * Returns base64 xdr representation of this object 14 | */ 15 | fun toBase64(): String { 16 | val outputStream = ByteArrayOutputStream() 17 | val xdrOutputStream = XdrDataOutputStream(outputStream) 18 | this.toXdr(xdrOutputStream) 19 | val xdrBytes = outputStream.toByteArray() 20 | return String(Base64.encode(xdrBytes)) 21 | } 22 | } -------------------------------------------------------------------------------- /generateXDR.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Print the usage message 4 | function printHelp () { 5 | echo "Usage: " 6 | echo " generateXDR.sh " 7 | echo 8 | echo "Example of using" 9 | echo 10 | echo " generateXDR.sh master" 11 | echo " generateXDR.sh 84135ed642bff4965ce69f9a91f566d6d525188d" 12 | echo 13 | } 14 | 15 | if [ -z "$1" ]; then 16 | echo "please select branch or commit hash" 17 | printHelp 18 | exit 1 19 | fi 20 | 21 | language=kotlin 22 | revision=$1 23 | namespace="org.tokend.wallet.xdr" 24 | output_folder="src/main/kotlin/org/tokend/wallet/xdr" 25 | 26 | script_path=`dirname "$0"`; script_path=`eval "cd \"$script_path\" && pwd"` 27 | output_folder_full_path="$script_path/$output_folder" 28 | 29 | docker pull registry.gitlab.com/tokend/xdrgen-docker 30 | docker run --rm -v $output_folder_full_path:/opt/generated registry.gitlab.com/tokend/xdrgen-docker $language $revision $namespace 31 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/PublicKeyFactory.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet 2 | 3 | import org.tokend.wallet.xdr.AccountID 4 | import org.tokend.wallet.xdr.BalanceID 5 | import org.tokend.wallet.xdr.PublicKey 6 | import org.tokend.wallet.xdr.Uint256 7 | 8 | /** 9 | * Holds method to create XDR public keys from 10 | * [Base32Check] encoded string representations. 11 | */ 12 | object PublicKeyFactory { 13 | /** 14 | * Creates [PublicKey] from [Base32Check] encoded balance ID. 15 | */ 16 | @JvmStatic 17 | fun fromBalanceId(balanceId: String): BalanceID { 18 | return PublicKey.KeyTypeEd25519(Uint256(Base32Check.decodeBalanceId(balanceId))) 19 | } 20 | 21 | /** 22 | * Creates [PublicKey] from [Base32Check] encoded account ID. 23 | */ 24 | @JvmStatic 25 | fun fromAccountId(accountId: String): AccountID { 26 | return PublicKey.KeyTypeEd25519(Uint256(Base32Check.decodeAccountId(accountId))) 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/utils/ByteCharArray.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.utils 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.CharBuffer 5 | import java.nio.charset.Charset 6 | 7 | /** 8 | * Converts current char sequence to raw bytes representation by UTF-8. 9 | */ 10 | fun CharArray.toByteArray(): ByteArray { 11 | val charBuffer = CharBuffer.wrap(this) 12 | val byteBuffer = Charset.forName("UTF-8").encode(charBuffer) 13 | charBuffer.clear() 14 | val bytes = ByteArray(byteBuffer.remaining()) 15 | byteBuffer.get(bytes).clear() 16 | return bytes 17 | } 18 | 19 | /** 20 | * Converts current byte sequence to chars by UTF-8. 21 | */ 22 | fun ByteArray.toCharArray(): CharArray { 23 | val byteBuffer = ByteBuffer.wrap(this) 24 | val charBuffer = Charset.forName("UTF-8").decode(byteBuffer) 25 | byteBuffer.clear() 26 | val chars = CharArray(charBuffer.remaining()) 27 | charBuffer.get(chars).clear() 28 | return chars 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/CreateOfferOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.PublicKeyFactory 4 | import org.tokend.wallet.xdr.BalanceID 5 | import org.tokend.wallet.xdr.Int64 6 | import org.tokend.wallet.xdr.ManageOfferOp 7 | import org.tokend.wallet.xdr.Uint64 8 | 9 | class CreateOfferOp : ManageOfferOp { 10 | @JvmOverloads 11 | constructor( 12 | baseBalance: BalanceID, 13 | quoteBalance: BalanceID, 14 | baseAmount: Int64, 15 | price: Int64, 16 | fee: Int64, 17 | isBuy: Boolean, 18 | orderBookId: Uint64 = 0 19 | ) : super(baseBalance, quoteBalance, isBuy, baseAmount, price, fee, 20 | 0L, orderBookId, ManageOfferOpExt.EmptyVersion()) 21 | 22 | @JvmOverloads 23 | constructor( 24 | baseBalanceId: String, 25 | quoteBalanceId: String, 26 | baseAmount: Int64, 27 | price: Int64, 28 | fee: Int64, 29 | isBuy: Boolean, 30 | orderBookId: Uint64 = 0L 31 | ) : this( 32 | PublicKeyFactory.fromBalanceId(baseBalanceId), 33 | PublicKeyFactory.fromBalanceId(quoteBalanceId), 34 | baseAmount, price, fee, isBuy, orderBookId 35 | ) 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/SimplePaymentOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.PublicKeyFactory 4 | import org.tokend.wallet.xdr.* 5 | 6 | class SimplePaymentOp : PaymentOp { 7 | @JvmOverloads 8 | constructor( 9 | sourceBalanceId: BalanceID, 10 | destAccountId: AccountID, 11 | amount: Int64, 12 | feeData: PaymentFeeData, 13 | subject: String256 = "", 14 | reference: Longstring = "" 15 | ) : super( 16 | sourceBalanceId, 17 | PaymentOpDestination.Account(destAccountId), 18 | amount, 19 | feeData, 20 | subject, 21 | reference, 22 | PaymentOpExt.EmptyVersion()) 23 | 24 | @JvmOverloads 25 | constructor( 26 | sourceBalanceId: String, 27 | destAccountId: String, 28 | amount: Long, 29 | feeData: PaymentFeeData, 30 | subject: String = "", 31 | reference: Longstring = "" 32 | ) : this( 33 | PublicKeyFactory.fromBalanceId(sourceBalanceId), 34 | PublicKeyFactory.fromAccountId(destAccountId), 35 | amount, 36 | feeData, 37 | subject, 38 | reference 39 | ) 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/op_extensions/SimpleSetFeesOp.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.op_extensions 2 | 3 | import org.tokend.wallet.PublicKeyFactory 4 | import org.tokend.wallet.utils.Hashing 5 | import org.tokend.wallet.xdr.* 6 | 7 | open class SimpleSetFeesOp : SetFeesOp { 8 | @JvmOverloads 9 | constructor( 10 | isDelete: Boolean, 11 | type: FeeType, 12 | asset: String, 13 | fixed: Int64, 14 | percent: Int64, 15 | upperBound: Int64, 16 | lowerBound: Int64, 17 | subtype: Int64 = 0L, 18 | accountId: String? = null, 19 | accountRole: Uint64? = null 20 | 21 | ) : super( 22 | FeeEntry( 23 | type, 24 | asset, 25 | fixed, 26 | percent, 27 | if (accountId != null) 28 | PublicKeyFactory.fromAccountId(accountId) 29 | else 30 | null, 31 | accountRole, 32 | subtype, 33 | lowerBound, 34 | upperBound, 35 | getHash(type, asset, subtype, accountId, accountRole), 36 | FeeEntry.FeeEntryExt.EmptyVersion() 37 | ), 38 | isDelete, 39 | SetFeesOpExt.EmptyVersion() 40 | ) 41 | 42 | companion object { 43 | @JvmStatic 44 | fun getHash(type: FeeType, asset: String, subtype: Int64, 45 | accountId: String?, accountRole: Uint64?): Hash { 46 | var data = 47 | "type:${type.value}asset:${asset}subtype:$subtype" 48 | 49 | if (accountId != null) { 50 | data += "accountID:$accountId" 51 | } 52 | 53 | if (accountRole != null) { 54 | data += "accountRole:$accountRole" 55 | } 56 | 57 | return Hash( 58 | Hashing.sha256(data.toByteArray()) 59 | ) 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /deploy.gradle: -------------------------------------------------------------------------------- 1 | // Config for private Maven repo deployment. 2 | 3 | if (file("repoCredentials.gradle").exists()) { 4 | apply from: "repoCredentials.gradle" 5 | } 6 | 7 | apply plugin: 'maven' 8 | 9 | task sourceJar(type: Jar, dependsOn: classes) { 10 | classifier 'sources' 11 | from sourceSets.main.allSource 12 | } 13 | 14 | task javadocJar(type: Jar, dependsOn: javadoc) { 15 | classifier = 'javadoc' 16 | from javadoc.destinationDir 17 | } 18 | 19 | artifacts { 20 | archives javadocJar, sourceJar 21 | } 22 | 23 | configurations { 24 | deployerJars 25 | } 26 | 27 | dependencies { 28 | deployerJars "org.apache.maven.wagon:wagon-ssh:2.2" 29 | } 30 | 31 | uploadArchives { 32 | repositories.mavenDeployer { 33 | dependsOn test 34 | 35 | configuration = configurations.deployerJars 36 | 37 | beforeDeployment { 38 | if (!(project.ext.has("repoUrl") 39 | && project.ext.has("repoUser") 40 | && project.ext.has("repoPass"))) { 41 | throw new GradleException("In order to deploy archives you must create " + 42 | "'repoCredentials.gradle' file the root dir and specify following " + 43 | "credentials inside:\n" + 44 | "\text.repoUrl\n\text.repoUser\n\text.repoPass") 45 | } 46 | } 47 | 48 | repository(url: project.ext.has("repoUrl") ? repoUrl : "") { 49 | authentication(userName: project.ext.has("repoUser") ? repoUser : "", 50 | password: project.ext.has("repoPass") ? repoPass : "") 51 | } 52 | 53 | pom.project { 54 | licenses { 55 | license { 56 | name "Apache License, Version 2.0" 57 | url "https://www.apache.org/licenses/LICENSE-2.0.txt" 58 | distribution "repo" 59 | } 60 | } 61 | organization { 62 | name "Distributed Lab" 63 | url "https://distributedlab.com" 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrPrimitives.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | // Int32 and UInt32 4 | fun Int.toXdr(stream: XdrDataOutputStream) { 5 | stream.writeInt(this) 6 | } 7 | 8 | fun Int.Companion.fromXdr(stream: XdrDataInputStream): Int { 9 | return stream.readInt() 10 | } 11 | 12 | // Int64 and UInt64 13 | fun Long.toXdr(stream: XdrDataOutputStream) { 14 | stream.writeLong(this) 15 | } 16 | 17 | fun Long.Companion.fromXdr(stream: XdrDataInputStream): Long { 18 | return stream.readLong() 19 | } 20 | 21 | // String 22 | fun String.toXdr(stream: XdrDataOutputStream) { 23 | stream.writeString(this) 24 | } 25 | 26 | fun String.Companion.fromXdr(stream: XdrDataInputStream): String { 27 | return stream.readString() 28 | } 29 | 30 | // Bool 31 | fun Boolean.toXdr(stream: XdrDataOutputStream) { 32 | stream.writeInt(if (this) 1 else 0) 33 | } 34 | 35 | fun Boolean.Companion.fromXdr(stream: XdrDataInputStream): Boolean { 36 | return stream.readInt() == 1 37 | } 38 | 39 | // Opaque 40 | fun ByteArray.toXdr(stream: XdrDataOutputStream) { 41 | this.size.toXdr(stream) 42 | stream.write(this) 43 | } 44 | 45 | object XdrOpaque { 46 | @JvmStatic 47 | fun fromXdr(stream: XdrDataInputStream): ByteArray { 48 | val size = stream.readInt() 49 | val array = ByteArray(size) 50 | stream.read(array) 51 | return array 52 | } 53 | } 54 | 55 | /** 56 | * Fixed size opaque data 57 | */ 58 | abstract class XdrFixedByteArray : XdrEncodable { 59 | var wrapped: ByteArray 60 | set(value) { 61 | when { 62 | value.size == this.size -> 63 | field = value 64 | value.size > this.size -> 65 | // TODO: Throw exception 66 | field = value.sliceArray(0..(this.size - 1)) 67 | value.size < this.size -> { 68 | field = ByteArray(this.size) 69 | value.forEachIndexed { index, el -> 70 | field[index] = el 71 | } 72 | } 73 | } 74 | } 75 | /** 76 | * Size of specific fixed opaque type. Should be overridden in child classes 77 | */ 78 | abstract val size: Int 79 | 80 | constructor(wrapped: ByteArray) { 81 | this.wrapped = wrapped 82 | } 83 | 84 | override fun toXdr(stream: XdrDataOutputStream) { 85 | stream.write(this.wrapped) 86 | } 87 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TokenD Kotlin wallet 2 | 3 | This library implements transactions and keys management for TokenD-related projects. 4 | 5 | ## Installation 6 | 7 | For **Gradle** add following lines to your project's `build.gradle`: 8 | ```groovy 9 | allprojects { 10 | repositories { 11 | ... 12 | maven { url "https://maven.tokend.io" } 13 | } 14 | } 15 | 16 | dependencies { 17 | ... 18 | compile "org.tokend:wallet:3.7.0" 19 | } 20 | 21 | ``` 22 | 23 | ## Usage examples 24 | 25 | Key management and signing: 26 | 27 | ```kotlin 28 | val SEED = "SBUFJEEK7FMWXPE4HGOWQZPHZ4V5TFKGSF664RAGT24NS662MKTQ7J6S".toCharArray() 29 | private val DATA = "TokenD is awesome".toByteArray() 30 | 31 | val account = Account.fromSecretSeed(SEED) 32 | val decoratedSignature = account.signDecorated(DATA) 33 | ``` 34 | 35 | Transaction creation: 36 | 37 | ```kotlin 38 | val SEED = "SBUFJEEK7FMWXPE4HGOWQZPHZ4V5TFKGSF664RAGT24NS662MKTQ7J6S".toCharArray() 39 | val NETWORK = NetworkParams("Example Test Network") 40 | 41 | val sourceAccount = Account.fromSecretSeed(SEED) 42 | val operation = CreateBalanceOp(SOURCE_ACCOUNT_ID, "OLE") 43 | 44 | val transaction = TransactionBuilder(NETWORK, sourceAccount.accountId) 45 | .addOperation(Operation.OperationBody.ManageBalance(operation)) 46 | .setMemo(Memo.MemoText("TokenD is awesome")) 47 | .addSigner(sourceAccount) 48 | .build() 49 | 50 | val envelope = transaction.getEnvelope().toBase64() 51 | ``` 52 | 53 | Decoding: 54 | ```kotlin 55 | val txResultEncoded = "AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAJAAAAAAAAAADMOKDasWPzpJIqN9sWipdvcjEZRTnGBvUezXbEd6rKMAAAAAAAAAAA" 56 | val txResult = TransactionResult.fromBase64(txResultEncoded) 57 | ``` 58 | 59 | ## ProGuard 60 | As long as you don't decode XDRs (`.fromXdr(...)`) no extra ProGuard 61 | rules are required. Otherwise add following lines to your 62 | project's `proguard-rules.pro`: 63 | ```proguard 64 | # Wallet 65 | -keep class org.tokend.wallet.xdr.* { *; } 66 | -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations 67 | ``` 68 | ## XDR Update 69 | XDR sources are located in [TokenD XDR repository](https://github.com/tokend/xdr/). 70 | You can generate new XDRs using our Docker-based XDR generator. 71 | [Docker](https://www.docker.com/) is required to perform this action. 72 | 73 | In order to generate new XDRs run `generateXDR` script with a source revision (tag or branch or commit) as an argument: 74 | 75 | ```bash 76 | ./generateXDR.sh master 77 | ``` 78 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/Base32CheckTest.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import com.google.common.io.BaseEncoding 4 | import org.junit.Assert 5 | import org.junit.Test 6 | import org.tokend.wallet.Base32Check 7 | import java.security.SecureRandom 8 | 9 | class Base32CheckTest { 10 | val SEED_ENCODED = "SDJHRQF4GCMIIKAAAQ6IHY42X73FQFLHUULAPSKKD4DFDM7UXWWCRHBE" 11 | val ACCOUNT_ID_ENCODED = "GDJHRQF4GCMIIKAAAQ6IHY42X73FQFLHUULAPSKKD4DFDM7UXWWCQDS3" 12 | val BALANCE_ID_ENCODED = "BDJHRQF4GCMIIKAAAQ6IHY42X73FQFLHUULAPSKKD4DFDM7UXWWCQMUQ" 13 | val BYTES = BaseEncoding.base16().decode("D278C0BC3098842800043C83E39ABFF6581567A51607C94A1F0651B3F4BDAC28") 14 | 15 | @Test 16 | fun encodeSeed() { 17 | val encoded = String(Base32Check.encodeSecretSeed(BYTES)) 18 | Assert.assertEquals(SEED_ENCODED, encoded) 19 | } 20 | 21 | @Test 22 | fun decodeSeed() { 23 | val decoded = Base32Check.decodeSecretSeed(SEED_ENCODED.toCharArray()) 24 | Assert.assertArrayEquals(BYTES, decoded) 25 | } 26 | 27 | @Test 28 | fun encodeAccountId() { 29 | val encoded = Base32Check.encodeAccountId(BYTES) 30 | Assert.assertEquals(ACCOUNT_ID_ENCODED, encoded) 31 | } 32 | 33 | @Test 34 | fun decodeAccountId() { 35 | val decoded = Base32Check.decodeAccountId(ACCOUNT_ID_ENCODED) 36 | Assert.assertArrayEquals(BYTES, decoded) 37 | } 38 | 39 | @Test 40 | fun encodeBalanceId() { 41 | val encoded = Base32Check.encodeBalanceId(BYTES) 42 | Assert.assertEquals(BALANCE_ID_ENCODED, encoded) 43 | } 44 | 45 | @Test 46 | fun decodeBalanceId() { 47 | val decoded = Base32Check.decodeBalanceId(BALANCE_ID_ENCODED) 48 | Assert.assertArrayEquals(BYTES, decoded) 49 | } 50 | 51 | @Test 52 | fun decodeInvalidVersionByte() { 53 | try { 54 | Base32Check.decodeAccountId(SEED_ENCODED) 55 | Assert.fail() 56 | } catch (e: Base32Check.FormatException) { 57 | } 58 | } 59 | 60 | @Test 61 | fun testValidation() { 62 | Assert.assertTrue(Base32Check.isValid(Base32Check.VersionByte.ACCOUNT_ID, ACCOUNT_ID_ENCODED.toCharArray())) 63 | Assert.assertFalse(Base32Check.isValid(Base32Check.VersionByte.BALANCE_ID, ACCOUNT_ID_ENCODED.toCharArray())) 64 | } 65 | 66 | @Test 67 | fun randomData() { 68 | val source = SecureRandom.getSeed(50) 69 | val encoded = Base32Check.encodeAccountId(source) 70 | val decoded = Base32Check.decodeAccountId(encoded) 71 | Assert.assertArrayEquals(source, decoded) 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/utils/SecureCharArrayWriter.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.utils 2 | 3 | import org.tokend.crypto.ecdsa.erase 4 | import java.io.CharArrayWriter 5 | 6 | /** 7 | * [CharArrayWriter] that does not leave buffer copies 8 | * on size expansion. 9 | */ 10 | class SecureCharArrayWriter(initialSize: Int) : CharArrayWriter(initialSize) { 11 | /** 12 | * Writes a character to the buffer. 13 | */ 14 | override fun write(c: Int) { 15 | synchronized(lock) { 16 | val newCount = count + 1 17 | if (newCount > buf.size) { 18 | val toErase = buf 19 | buf = buf.copyOf(Math.max(buf.size shl 1, newCount)) 20 | toErase.erase() 21 | } 22 | buf[count] = c.toChar() 23 | count = newCount 24 | } 25 | } 26 | 27 | /** 28 | * Writes characters to the buffer. 29 | * @param c the data to be written 30 | * @param off the start offset in the data 31 | * @param len the number of chars that are written 32 | */ 33 | override fun write(c: CharArray, off: Int, len: Int) { 34 | if (off < 0 || off > c.size || len < 0 || 35 | off + len > c.size || off + len < 0) { 36 | throw IndexOutOfBoundsException() 37 | } else if (len == 0) { 38 | return 39 | } 40 | synchronized(lock) { 41 | val newCount = count + len 42 | if (newCount > buf.size) { 43 | val toErase = buf 44 | buf = buf.copyOf(Math.max(buf.size shl 1, newCount)) 45 | toErase.erase() 46 | } 47 | System.arraycopy(c, off, buf, count, len) 48 | count = newCount 49 | } 50 | } 51 | 52 | /** 53 | * Write a portion of a string to the buffer. 54 | * @param str String to be written from 55 | * @param off Offset from which to start reading characters 56 | * @param len Number of characters to be written 57 | */ 58 | override fun write(str: String, off: Int, len: Int) { 59 | synchronized(lock) { 60 | val newCount = count + len 61 | if (newCount > buf.size) { 62 | val toErase = buf 63 | buf = buf.copyOf(Math.max(buf.size shl 1, newCount)) 64 | toErase.erase() 65 | } 66 | str.toCharArray(buf, count, off, off + len) 67 | count = newCount 68 | } 69 | } 70 | 71 | /** 72 | * Erases the buffer and resets chars count to 0. 73 | */ 74 | fun erase() { 75 | close() 76 | buf.erase() 77 | reset() 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrDataOutputStream.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | import java.io.DataOutputStream 4 | import java.io.IOException 5 | import java.io.OutputStream 6 | 7 | class XdrDataOutputStream(out: OutputStream) : DataOutputStream(XdrOutputStream(out)) { 8 | 9 | private val mOut: XdrOutputStream 10 | 11 | init { 12 | mOut = super.out as XdrOutputStream 13 | } 14 | 15 | @Throws(IOException::class) 16 | fun writeString(s: String) { 17 | val chars = s.toByteArray(Charsets.UTF_8) 18 | writeInt(chars.size) 19 | write(chars) 20 | pad() 21 | } 22 | 23 | @Throws(IOException::class) 24 | fun writeIntArray(a: IntArray) { 25 | writeInt(a.size) 26 | writeIntArray(a, a.size) 27 | } 28 | 29 | @Throws(IOException::class) 30 | fun writeIntArray(a: IntArray, l: Int) { 31 | for (i in 0 until l) { 32 | writeInt(a[i]) 33 | } 34 | } 35 | 36 | @Throws(IOException::class) 37 | fun writeFloatArray(a: FloatArray) { 38 | writeInt(a.size) 39 | writeFloatArray(a, a.size) 40 | } 41 | 42 | @Throws(IOException::class) 43 | fun writeFloatArray(a: FloatArray, l: Int) { 44 | for (i in 0 until l) { 45 | writeFloat(a[i]) 46 | } 47 | } 48 | 49 | @Throws(IOException::class) 50 | fun writeDoubleArray(a: DoubleArray) { 51 | writeInt(a.size) 52 | writeDoubleArray(a, a.size) 53 | } 54 | 55 | @Throws(IOException::class) 56 | fun writeDoubleArray(a: DoubleArray, l: Int) { 57 | for (i in 0 until l) { 58 | writeDouble(a[i]) 59 | } 60 | } 61 | 62 | @Throws(IOException::class) 63 | fun pad() { 64 | mOut.pad() 65 | } 66 | 67 | private class XdrOutputStream(private val mOut: OutputStream) : OutputStream() { 68 | 69 | private var mCount: Int = 0 70 | 71 | init { 72 | mCount = 0 73 | } 74 | 75 | @Throws(IOException::class) 76 | override fun write(b: Int) { 77 | mOut.write(b) 78 | mCount++ 79 | } 80 | 81 | @Throws(IOException::class) 82 | override fun write(b: ByteArray) { 83 | mOut.write(b) 84 | mCount += b.size 85 | } 86 | 87 | @Throws(IOException::class) 88 | override fun write(b: ByteArray, offset: Int, length: Int) { 89 | mOut.write(b, offset, length) 90 | mCount += length 91 | } 92 | 93 | @Throws(IOException::class) 94 | fun pad() { 95 | var pad = 0 96 | val mod = mCount % 4 97 | if (mod > 0) { 98 | pad = 4 - mod 99 | } 100 | while (pad-- > 0) { 101 | write(0) 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/TransactionTest.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import org.tokend.wallet.Account 6 | import org.tokend.wallet.NetworkParams 7 | import org.tokend.wallet.PublicKeyFactory 8 | import org.tokend.wallet.Transaction 9 | import org.tokend.wallet.utils.Base64 10 | import org.tokend.wallet.xdr.* 11 | import org.tokend.wallet.xdr.op_extensions.SimplePaymentOp 12 | 13 | class TransactionTest { 14 | private val sourceAccountId = "GDVJSBSBSERR3YP3LKLHTODWEFGCSLDWDIODER3CKLZXUMVPZOPT4MHY" 15 | private val network = NetworkParams("Example Test Network") 16 | 17 | private val sourceBalance = "BBVRUASMC2OMFGWHQPD4TTXTZZ7ACOFWWFTB5Y3K6757FSUSAEPEPXAS" 18 | private val destAccount = "GDBTAGESMWHT2OISMGJ27HB6WQB2FVNEEIZL2SRBD2CXN26L6J4NKDP2" 19 | private val sourceAccountSeed = "SBEBZQIXHAZ3BZXOJEN6R57KMEDISGBIIP6LAVRCNDM4WZIQPHNYZICC".toCharArray() 20 | private val account = Account.fromSecretSeed(sourceAccountSeed) 21 | 22 | private val paymentOp = SimplePaymentOp( 23 | sourceBalanceId = sourceBalance, 24 | destAccountId = destAccount, 25 | amount = 1 * 1000000L, 26 | feeData = PaymentFeeData( 27 | Fee(0L, 0L, Fee.FeeExt.EmptyVersion()), 28 | Fee(0L, 0L, Fee.FeeExt.EmptyVersion()), 29 | false, 30 | PaymentFeeData.PaymentFeeDataExt.EmptyVersion() 31 | ), 32 | subject = "Test" 33 | ) 34 | 35 | private val sampleTransaction = Transaction( 36 | network, 37 | PublicKeyFactory.fromAccountId(sourceAccountId), 38 | listOf(Operation(null, Operation.OperationBody.Payment(paymentOp))), 39 | Memo.MemoText("Sample text"), 40 | TimeBounds(0L, 42L), 41 | 0L 42 | ).apply { addSignature(account) } 43 | 44 | @Test 45 | fun encoding() { 46 | val transaction = sampleTransaction 47 | 48 | val expectedEnvelope = "AAAAAOqZBkGRIx3h+1qWebh2IUwpLHYaHDJHYlLzejKvy58+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqAAAAAQAAAAtTYW1wbGUgdGV4dAAAAAABAAAAAAAAABcAAAAAaxoCTBacwprHg8fJzvPOfgE4trFmHuNq9/vyypIBHkcAAAAAAAAAAMMwGJJljz05EmGTr5w+tAOi1aQiMr1KIR6FduvL8njVAAAAAAAPQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEVGVzdAAAAAAAAAAAAAAAAAAAAAHjQTipAAAAQF34oMJ+LK2Zu5FQIxbCsETYs5ELbzp4QsjS/5iu5rwxSiNtKBOGPwN43O57bMOEetTYdPWC+J2BKASM7eXVKQ0=" 49 | val envelope = transaction.getEnvelope().toBase64() 50 | Assert.assertEquals(expectedEnvelope, envelope) 51 | } 52 | 53 | @Test 54 | fun noOperations() { 55 | try { 56 | Transaction( 57 | network, 58 | PublicKeyFactory.fromAccountId(sourceAccountId), 59 | emptyList()) 60 | Assert.fail("Transactions with no operations can't be allowed") 61 | } catch (e: Exception) { 62 | } 63 | } 64 | 65 | @Test 66 | fun hash() { 67 | val expectedHash = "TcNNk7QSlWHviChZnnuwUp5tE6BxXL2BhFpWD6/4k3M=" 68 | val hash = Base64.encode(sampleTransaction.getHash()).toString(Charsets.UTF_8) 69 | Assert.assertEquals(expectedHash, hash) 70 | } 71 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/NetworkParams.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import org.tokend.wallet.NetworkParams 6 | import java.io.ByteArrayInputStream 7 | import java.io.ByteArrayOutputStream 8 | import java.io.ObjectInputStream 9 | import java.io.ObjectOutputStream 10 | import java.math.BigDecimal 11 | import java.util.* 12 | 13 | class NetworkParams { 14 | @Test 15 | fun amountConversion() { 16 | val precision = 6 17 | val amounts = listOf( 18 | BigDecimal("1.234567"), 19 | BigDecimal("18446744073709.551615"), 20 | BigDecimal.ONE, 21 | BigDecimal.TEN, 22 | BigDecimal.ZERO, 23 | BigDecimal("18446744073709.551") 24 | ) 25 | val networkParams = NetworkParams("Test phrase", precision) 26 | 27 | amounts.forEach { amount -> 28 | Assert.assertEquals( 29 | amount, 30 | networkParams.amountFromPrecised(networkParams.amountToPrecised(amount)) 31 | ) 32 | } 33 | } 34 | 35 | @Test 36 | fun amountConversionZeroPrecision() { 37 | val networkParams = NetworkParams("Test phrase", 0) 38 | val amount = BigDecimal("18446744073709551615") 39 | 40 | Assert.assertEquals( 41 | amount, 42 | networkParams.amountFromPrecised(networkParams.amountToPrecised(amount)) 43 | ) 44 | 45 | val amountToCut = BigDecimal("184467440.777") 46 | val cutAmount = BigDecimal("184467440") 47 | 48 | Assert.assertEquals( 49 | cutAmount, 50 | networkParams.amountFromPrecised(networkParams.amountToPrecised(amountToCut)) 51 | ) 52 | } 53 | 54 | @Test(expected = IllegalArgumentException::class) 55 | fun amountConversionNegative() { 56 | NetworkParams("Test phrase", 0) 57 | .amountToPrecised(BigDecimal("-1")) 58 | } 59 | 60 | @Test(expected = IllegalStateException::class) 61 | fun amountConversionOverflow() { 62 | NetworkParams("Test phrase", 0) 63 | .amountToPrecised(BigDecimal("18446744073709551619")) 64 | } 65 | 66 | @Test(expected = IllegalArgumentException::class) 67 | fun tooBigPrecision() { 68 | NetworkParams("Test phrase", 8) 69 | } 70 | 71 | @Test 72 | fun timeCorrection() { 73 | val correction = 60 74 | val networkParams = NetworkParams("Test phrase", 75 | NetworkParams.MAX_PRECISION, correction) 76 | 77 | val actual = Date().time / 1000 78 | val calculated = networkParams.nowTimestamp 79 | 80 | Assert.assertTrue((calculated - actual) in (correction - 1..correction + 1)) 81 | } 82 | 83 | @Test 84 | fun serialization() { 85 | val networkParams = NetworkParams("Test phrase", 4, 42) 86 | val byteArrayOutputStream = ByteArrayOutputStream() 87 | ObjectOutputStream(byteArrayOutputStream).writeObject(networkParams) 88 | val inputStream = ByteArrayInputStream(byteArrayOutputStream.toByteArray()) 89 | val pn = ObjectInputStream(inputStream).readObject() as NetworkParams 90 | 91 | Assert.assertEquals(networkParams.passphrase, pn.passphrase) 92 | Assert.assertEquals(networkParams.precision, pn.precision) 93 | Assert.assertEquals(networkParams.timeOffsetSeconds, pn.timeOffsetSeconds) 94 | Assert.assertArrayEquals(networkParams.networkId, pn.networkId) 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/utils/Base64.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.utils 2 | 3 | import java.io.ByteArrayOutputStream 4 | 5 | object Base64 { 6 | private val encodeTable = (CharRange('A', 'Z') + CharRange('a', 'z') + CharRange('0', '9') + '+' + '/').toCharArray() 7 | 8 | private val decodeTable = intArrayOf(-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 9 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 10 | -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 11 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, 12 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 13 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 14 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 15 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1) 16 | 17 | fun encode(data: ByteArray): ByteArray { 18 | val output = ByteArrayOutputStream() 19 | var padding = 0 20 | var position = 0 21 | while (position < data.size) { 22 | var b = data[position].toInt() and 0xFF shl 16 and 0xFFFFFF 23 | if (position + 1 < data.size) b = b or (data[position + 1].toInt() and 0xFF shl 8) else padding++ 24 | if (position + 2 < data.size) b = b or (data[position + 2].toInt() and 0xFF) else padding++ 25 | for (i in 0 until 4 - padding) { 26 | val c = b and 0xFC0000 shr 18 27 | output.write(encodeTable[c].toInt()) 28 | b = b shl 6 29 | } 30 | position += 3 31 | } 32 | for (i in 0 until padding) { 33 | output.write('='.toInt()) 34 | } 35 | return output.toByteArray() 36 | } 37 | 38 | fun decode(encoded: ByteArray): ByteArray { 39 | val output = ByteArrayOutputStream() 40 | var position = 0 41 | while (position < encoded.size) { 42 | var b: Int 43 | if (decodeTable[encoded[position].toInt()] != -1) { 44 | b = decodeTable[encoded[position].toInt()] and 0xFF shl 18 45 | } else { 46 | position++ 47 | continue 48 | } 49 | var count = 0 50 | if (position + 1 < encoded.size && decodeTable[encoded[position + 1].toInt()] != -1) { 51 | b = b or (decodeTable[encoded[position + 1].toInt()] and 0xFF shl 12) 52 | count++ 53 | } 54 | if (position + 2 < encoded.size && decodeTable[encoded[position + 2].toInt()] != -1) { 55 | b = b or (decodeTable[encoded[position + 2].toInt()] and 0xFF shl 6) 56 | count++ 57 | } 58 | if (position + 3 < encoded.size && decodeTable[encoded[position + 3].toInt()] != -1) { 59 | b = b or (decodeTable[encoded[position + 3].toInt()] and 0xFF) 60 | count++ 61 | } 62 | while (count > 0) { 63 | val c = b and 0xFF0000 shr 16 64 | output.write(c.toChar().toInt()) 65 | b = b shl 8 66 | count-- 67 | } 68 | position += 4 69 | } 70 | return output.toByteArray() 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/NetworkParams.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet 2 | 3 | import org.tokend.wallet.utils.Hashing 4 | import java.io.Serializable 5 | import java.math.BigDecimal 6 | import java.math.BigInteger 7 | import java.math.MathContext 8 | import java.math.RoundingMode 9 | import java.util.* 10 | 11 | /** 12 | * Holds network-specific parameters. 13 | */ 14 | class NetworkParams : Serializable { 15 | /** 16 | * Passphrase of the network 17 | */ 18 | val passphrase: String 19 | 20 | /** 21 | * Decimal places in amounts. For example, 0.000001 in 6 precision is 1 22 | */ 23 | val precision: Int 24 | 25 | /** 26 | * Multiplier for precised amount conversions 27 | * 28 | * @see precision 29 | */ 30 | val precisionMultiplier: Long 31 | 32 | /** 33 | * Identifier of the network 34 | */ 35 | val networkId: ByteArray 36 | 37 | /** 38 | * Offset between device and server time in seconds 39 | */ 40 | val timeOffsetSeconds: Int 41 | 42 | /** 43 | * Calculated current time on server as a UNIX timestamp 44 | * 45 | * @see timeOffsetSeconds 46 | */ 47 | val nowTimestamp: Long 48 | get() = (Date().time / 1000L) + timeOffsetSeconds 49 | 50 | /** 51 | * @param passphrase network passphrase 52 | * @param precision decimal places in amounts, [DEFAULT_PRECISION] by default, [MAX_PRECISION] max 53 | * @param timeOffsetSeconds offset between device and server time in seconds, 0 by default 54 | */ 55 | @JvmOverloads 56 | constructor(passphrase: String, 57 | precision: Int = DEFAULT_PRECISION, 58 | timeOffsetSeconds: Int = 0) { 59 | require(precision <= MAX_PRECISION) { "Precision can't be bigger than $MAX_PRECISION" } 60 | 61 | this.passphrase = passphrase 62 | this.precision = precision 63 | this.precisionMultiplier = BigDecimal.TEN.pow(precision).longValueExact() 64 | this.networkId = Hashing.sha256(passphrase.toByteArray()) 65 | this.timeOffsetSeconds = timeOffsetSeconds 66 | } 67 | 68 | /** 69 | * Converts given amount to network format. 70 | * 71 | * @return UInt64 value in [Long], may result in negative Java value 72 | * if the precised amount is bigger than 9223372036854775807 73 | * 74 | * @see NetworkParams.precision 75 | */ 76 | fun amountToPrecised(amount: BigDecimal): Long { 77 | require(amount.signum() >= 0) { "Amount can't be negative" } 78 | 79 | return amount 80 | .multiply(BigDecimal(precisionMultiplier)) 81 | .setScale(0, RoundingMode.DOWN) 82 | .also { check(it <= MAX_PRECISED_AMOUNT) { "$it overflows UInt64" } } 83 | .toLong() 84 | } 85 | 86 | /** 87 | * Converts given amount from network format to human-readable. 88 | * 89 | * @param amount UInt64 value in [Long], 90 | * negative Java values are treated as bigger than 9223372036854775807 91 | */ 92 | fun amountFromPrecised(amount: Long): BigDecimal { 93 | val amountBytes = ByteArray(Long.SIZE_BYTES).apply { 94 | for (byteI in 0 until size) { 95 | set(byteI, amount.ushr((size - 1 - byteI) * 8).toByte()) 96 | } 97 | } 98 | 99 | return BigDecimal(BigInteger(1, amountBytes)) 100 | .divide(BigDecimal(precisionMultiplier), MathContext.DECIMAL128) 101 | } 102 | 103 | companion object { 104 | private val MAX_PRECISED_AMOUNT = BigDecimal("18446744073709551615") 105 | 106 | const val serialVersionUID = 5677019745177892600L 107 | 108 | const val MAX_PRECISION = 6 109 | 110 | /** 111 | * Default amount precision in TokenD. 112 | */ 113 | const val DEFAULT_PRECISION = MAX_PRECISION 114 | } 115 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/XdrModelsTests.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import org.tokend.wallet.Base32Check 6 | import org.tokend.wallet.PublicKeyFactory 7 | import org.tokend.wallet.xdr.* 8 | import org.tokend.wallet.xdr.op_extensions.SimplePaymentOp 9 | 10 | class XdrModelsTests { 11 | private val ACCOUNT_ID = "GB5V4R2P6EPS7VDDTWATJKSS3F4FWRIPNRKCWFL6WSYIWBN3L6YH3A3J" 12 | private val BALANCE_ID = "BA7UXH23ZELVU6XZFEXMAE3J4QJTGG3F5ZPOV2BJ335CMGO6BHWRODQG" 13 | private val ASSET_CODE = "OLG" 14 | private val AMOUNT = 12345600L 15 | private val SUPER_AMOUNT = -1L 16 | private val BYTES_ACCOUNT_ID: PublicKey = PublicKey.KeyTypeEd25519(Uint256(Base32Check.decodeAccountId(ACCOUNT_ID))) 17 | private val BYTES_BALANCE_ID: PublicKey = PublicKey.KeyTypeEd25519(Uint256(Base32Check.decodeBalanceId(BALANCE_ID))) 18 | 19 | @Test 20 | fun testCreateBalanceOp() { 21 | val manageBalanceOp = ManageBalanceOp(ManageBalanceAction.CREATE, BYTES_ACCOUNT_ID, ASSET_CODE, 22 | ManageBalanceOp.ManageBalanceOpExt.EmptyVersion()) 23 | 24 | val op = Operation(null, Operation.OperationBody.ManageBalance(manageBalanceOp)) 25 | 26 | Assert.assertEquals("AAAAAAAAAAkAAAAAAAAAAHteR0/xHy/UY52BNKpS2XhbRQ9sVCsVfrSwiwW7X7B9AAAAA09MRwAAAAAA", 27 | op.toBase64()) 28 | } 29 | 30 | @Test 31 | fun testDeleteBalanceOp() { 32 | val manageBalanceOp = ManageBalanceOp(ManageBalanceAction.DELETE_BALANCE, BYTES_ACCOUNT_ID, ASSET_CODE, 33 | ManageBalanceOp.ManageBalanceOpExt.EmptyVersion()) 34 | 35 | val op = Operation(null, Operation.OperationBody.ManageBalance(manageBalanceOp)) 36 | 37 | Assert.assertEquals("AAAAAAAAAAkAAAABAAAAAHteR0/xHy/UY52BNKpS2XhbRQ9sVCsVfrSwiwW7X7B9AAAAA09MRwAAAAAA", 38 | op.toBase64()) 39 | } 40 | 41 | @Test 42 | fun testPaymentOp() { 43 | val paymentOp = SimplePaymentOp( 44 | sourceBalanceId = BYTES_BALANCE_ID, 45 | destAccountId = BYTES_ACCOUNT_ID, 46 | amount = AMOUNT, 47 | feeData = PaymentFeeData( 48 | Fee(0L, 0L, Fee.FeeExt.EmptyVersion()), 49 | Fee(0L, 0L, Fee.FeeExt.EmptyVersion()), 50 | false, 51 | PaymentFeeData.PaymentFeeDataExt.EmptyVersion() 52 | ), 53 | subject = "Test" 54 | ) 55 | 56 | val op = Operation(null, Operation.OperationBody.Payment(paymentOp)) 57 | 58 | Assert.assertEquals("AAAAAAAAABcAAAAAP0ufW8kXWnr5KS7AE2nkEzMbZe5e6ugp3vomGd4J7RcAAAAAAAAAAHteR0/xHy/UY52BNKpS2XhbRQ9sVCsVfrSwiwW7X7B9AAAAAAC8YQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEVGVzdAAAAAAAAAAA", 59 | op.toBase64()) 60 | } 61 | 62 | @Test 63 | fun testManageSignerOp() { 64 | val manageOp = ManageSignerOp( 65 | ManageSignerOp.ManageSignerOpData.Create( 66 | UpdateSignerData( 67 | publicKey = PublicKeyFactory.fromAccountId(ACCOUNT_ID), 68 | weight = 255, 69 | identity = 255, 70 | roleID = 255, 71 | details = "", 72 | ext = EmptyExt.EmptyVersion() 73 | ) 74 | ), 75 | EmptyExt.EmptyVersion() 76 | ) 77 | 78 | val op = Operation(null, Operation.OperationBody.ManageSigner(manageOp)) 79 | 80 | Assert.assertEquals("AAAAAAAAACYAAAAAAAAAAHteR0/xHy/UY52BNKpS2XhbRQ9sVCsVfrSwiwW7X7B9AAAAAAAAAP8AAAD/AAAA/wAAAAAAAAAAAAAAAA==", 81 | op.toBase64()) 82 | } 83 | 84 | @Test 85 | fun testTimeBounds() { 86 | val timeBounds = TimeBounds(0, SUPER_AMOUNT) 87 | 88 | Assert.assertEquals("AAAAAAAAAAD//////////w==", timeBounds.toBase64()) 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/XdrDataInputStream.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | import java.io.DataInputStream 4 | import java.io.IOException 5 | import java.io.InputStream 6 | 7 | class XdrDataInputStream 8 | /** 9 | * Creates a XdrDataInputStream that uses the specified 10 | * underlying InputStream. 11 | * 12 | * @param in the specified input stream 13 | */ 14 | (`in`: InputStream) : DataInputStream(XdrInputStream(`in`)) { 15 | 16 | // The underlying input stream 17 | private val mIn: XdrInputStream 18 | 19 | // The total bytes read so far. 20 | private val mCount: Int 21 | 22 | init { 23 | mIn = super.`in` as XdrInputStream 24 | mCount = 0 25 | } 26 | 27 | @Throws(IOException::class) 28 | fun readString(): String { 29 | val l = readInt() 30 | val bytes = ByteArray(l) 31 | readFully(bytes) 32 | pad() 33 | return String(bytes, Charsets.UTF_8) 34 | } 35 | 36 | @Throws(IOException::class) 37 | fun readIntArray(): IntArray { 38 | val l = readInt() 39 | return readIntArray(l) 40 | } 41 | 42 | @Throws(IOException::class) 43 | fun readIntArray(l: Int): IntArray { 44 | val arr = IntArray(l) 45 | for (i in 0 until l) { 46 | arr[i] = readInt() 47 | } 48 | return arr 49 | } 50 | 51 | @Throws(IOException::class) 52 | fun readFloatArray(): FloatArray { 53 | val l = readInt() 54 | return readFloatArray(l) 55 | } 56 | 57 | @Throws(IOException::class) 58 | fun readFloatArray(l: Int): FloatArray { 59 | val arr = FloatArray(l) 60 | for (i in 0 until l) { 61 | arr[i] = readFloat() 62 | } 63 | return arr 64 | } 65 | 66 | @Throws(IOException::class) 67 | fun readDoubleArray(): DoubleArray { 68 | val l = readInt() 69 | return readDoubleArray(l) 70 | } 71 | 72 | @Throws(IOException::class) 73 | fun readDoubleArray(l: Int): DoubleArray { 74 | val arr = DoubleArray(l) 75 | for (i in 0 until l) { 76 | arr[i] = readDouble() 77 | } 78 | return arr 79 | } 80 | 81 | /** 82 | * Skips ahead to bring the stream to 4 byte alignment. 83 | */ 84 | @Throws(IOException::class) 85 | fun pad() { 86 | mIn.pad() 87 | } 88 | 89 | @Throws(IOException::class) 90 | override fun read(): Int { 91 | return super.read() 92 | } 93 | 94 | /** 95 | * Need to provide a custom impl of InputStream as DataInputStream's read methods 96 | * are final and we need to keep track of the count for padding purposes. 97 | */ 98 | private class XdrInputStream(// The underlying input stream 99 | private val mIn: InputStream) : InputStream() { 100 | 101 | // The amount of bytes read so far. 102 | private var mCount: Int = 0 103 | 104 | init { 105 | mCount = 0 106 | } 107 | 108 | @Throws(IOException::class) 109 | override fun read(): Int { 110 | val read = mIn.read() 111 | if (read >= 0) { 112 | mCount++ 113 | } 114 | return read 115 | } 116 | 117 | @Throws(IOException::class) 118 | override fun read(b: ByteArray): Int { 119 | return read(b, 0, b.size) 120 | } 121 | 122 | @Throws(IOException::class) 123 | override fun read(b: ByteArray, off: Int, len: Int): Int { 124 | val read = mIn.read(b, off, len) 125 | mCount += read 126 | return read 127 | } 128 | 129 | @Throws(IOException::class) 130 | fun pad() { 131 | var pad = 0 132 | val mod = mCount % 4 133 | if (mod > 0) { 134 | pad = 4 - mod 135 | } 136 | skip(pad.toLong()) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/TransactionBuilder.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet 2 | 3 | import org.tokend.wallet.xdr.AccountID 4 | import org.tokend.wallet.xdr.Memo 5 | import org.tokend.wallet.xdr.Operation 6 | import org.tokend.wallet.xdr.TimeBounds 7 | 8 | /** 9 | * Builds a [Transaction] object. 10 | * 11 | * @param networkParams params of the network into which the transaction will be sent 12 | * @param sourceAccountId original account ID of the transaction initiator 13 | */ 14 | class TransactionBuilder(private val networkParams: NetworkParams, 15 | private val sourceAccountId: AccountID) { 16 | private val operations = mutableListOf() 17 | private var memo: Memo? = null 18 | private var timeBounds: TimeBounds? = null 19 | private var salt: Long? = null 20 | private val signers = mutableListOf() 21 | 22 | /** 23 | * @param networkParams params of the network into which the transaction will be sent 24 | * @param sourceAccountId original account ID of the transaction initiator 25 | */ 26 | constructor(networkParams: NetworkParams, 27 | sourceAccountId: String) : this(networkParams, PublicKeyFactory.fromAccountId(sourceAccountId)) 28 | 29 | /** 30 | * Adds operation with given body to the result transaction. 31 | * 32 | * @see Transaction.operations 33 | */ 34 | @JvmOverloads 35 | fun addOperation(operationBody: Operation.OperationBody, 36 | operationSourceAccount: AccountID? = null): TransactionBuilder { 37 | operations.add(Operation(operationSourceAccount, operationBody)) 38 | return this 39 | } 40 | 41 | /** 42 | * Adds operations with given bodies to the result transaction. 43 | * 44 | * @see Transaction.operations 45 | */ 46 | @JvmOverloads 47 | fun addOperations(operationBodies: Collection, 48 | operationsSourceAccount: AccountID? = null): TransactionBuilder { 49 | operations.addAll(operationBodies.map { 50 | Operation(operationsSourceAccount, it) 51 | }) 52 | return this 53 | } 54 | 55 | /** 56 | * Sets memo of the result transaction. 57 | * 58 | * @see Transaction.memo 59 | */ 60 | fun setMemo(memo: Memo): TransactionBuilder { 61 | this.memo = memo 62 | return this 63 | } 64 | 65 | /** 66 | * Sets range of time during which the 67 | * result transaction will be valid. 68 | * Default transaction lifetime is [Transaction.DEFAULT_LIFETIME_SECONDS] 69 | * @param timeBounds time range in unixtime 70 | * 71 | * @see Transaction.timeBounds 72 | */ 73 | fun setTimeBounds(timeBounds: TimeBounds): TransactionBuilder { 74 | this.timeBounds = timeBounds 75 | return this 76 | } 77 | 78 | /** 79 | * Sets salt of the result transaction. 80 | * By default transaction salt is a random [Long]. 81 | * 82 | * @see Transaction.salt 83 | */ 84 | fun setSalt(salt: Long): TransactionBuilder { 85 | this.salt = salt 86 | return this 87 | } 88 | 89 | /** 90 | * Adds given account as a signer of the result transaction 91 | * 92 | * @see Transaction.addSignature 93 | */ 94 | fun addSigner(signer: Account): TransactionBuilder { 95 | this.signers.add(signer) 96 | return this 97 | } 98 | 99 | /** 100 | * Builds the result transaction. 101 | * @throws IllegalStateException if no operations were added. 102 | */ 103 | fun build(): Transaction { 104 | val transaction = 105 | Transaction( 106 | networkParams, 107 | sourceAccountId, 108 | operations, 109 | memo, 110 | timeBounds, 111 | salt 112 | ) 113 | 114 | signers.forEach(transaction::addSignature) 115 | 116 | return transaction 117 | } 118 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/utils/SecureBase32.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.utils 2 | 3 | import kotlin.experimental.or 4 | 5 | object SecureBase32 { 6 | private val base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 7 | private val base32Lookup = intArrayOf(0xFF, 0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 8 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 9 | 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 10 | 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 11 | 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 12 | 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF) 13 | 14 | /** 15 | * Encodes byte array to Base32. 16 | * 17 | * @param bytes Bytes to encode. 18 | */ 19 | fun encode(bytes: ByteArray): CharArray { 20 | var i = 0 21 | var index = 0 22 | var digit: Int 23 | var currByte: Int 24 | var nextByte: Int 25 | val writer = SecureCharArrayWriter((bytes.size + 7) * 8 / 5) 26 | 27 | while (i < bytes.size) { 28 | currByte = if (bytes[i] >= 0) bytes[i].toInt() else bytes[i] + 256 29 | 30 | /* Is the current digit going to span a byte boundary? */ 31 | if (index > 3) { 32 | if (i + 1 < bytes.size) { 33 | nextByte = if (bytes[i + 1] >= 0) 34 | bytes[i + 1].toInt() 35 | else 36 | bytes[i + 1] + 256 37 | } else { 38 | nextByte = 0 39 | } 40 | 41 | digit = currByte and (0xFF shr index) 42 | index = (index + 5) % 8 43 | digit = digit shl index 44 | digit = digit or (nextByte shr 8 - index) 45 | i++ 46 | } else { 47 | digit = currByte shr 8 - (index + 5) and 0x1F 48 | index = (index + 5) % 8 49 | if (index == 0) 50 | i++ 51 | } 52 | writer.append(base32Chars[digit]) 53 | } 54 | 55 | val result = writer.toCharArray() 56 | writer.erase() 57 | 58 | return result 59 | } 60 | 61 | /** 62 | * Decodes the given Base32 chars to a raw byte array. 63 | * 64 | * @param base32 content to decode 65 | */ 66 | fun decode(base32: CharArray): ByteArray { 67 | var i: Int 68 | var index: Int 69 | var lookup: Int 70 | var offset: Int 71 | var digit: Int 72 | val bytes = ByteArray(base32.size * 5 / 8) 73 | 74 | i = 0 75 | index = 0 76 | offset = 0 77 | while (i < base32.size) { 78 | lookup = base32[i] - '0' 79 | 80 | /* Skip chars outside the lookup table */ 81 | if (lookup < 0 || lookup >= base32Lookup.size) { 82 | i++ 83 | continue 84 | } 85 | 86 | digit = base32Lookup[lookup] 87 | 88 | /* If this digit is not in the table, ignore it */ 89 | if (digit == 0xFF) { 90 | i++ 91 | continue 92 | } 93 | 94 | if (index <= 3) { 95 | index = (index + 5) % 8 96 | if (index == 0) { 97 | bytes[offset] = bytes[offset] or digit.toByte() 98 | offset++ 99 | if (offset >= bytes.size) 100 | break 101 | } else { 102 | bytes[offset] = bytes[offset] or (digit shl 8 - index).toByte() 103 | } 104 | } else { 105 | index = (index + 5) % 8 106 | bytes[offset] = bytes[offset] or digit.ushr(index).toByte() 107 | offset++ 108 | 109 | if (offset >= bytes.size) { 110 | break 111 | } 112 | bytes[offset] = bytes[offset] or (digit shl 8 - index).toByte() 113 | } 114 | i++ 115 | } 116 | return bytes 117 | } 118 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/TransactionBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import org.tokend.wallet.Account 6 | import org.tokend.wallet.NetworkParams 7 | import org.tokend.wallet.PublicKeyFactory 8 | import org.tokend.wallet.TransactionBuilder 9 | import org.tokend.wallet.xdr.* 10 | 11 | class TransactionBuilderTest { 12 | val SOURCE_ACCOUNT_ID = "GDVJSBSBSERR3YP3LKLHTODWEFGCSLDWDIODER3CKLZXUMVPZOPT4MHY" 13 | val SOURCE_ACCOUNT_PUBKEY = PublicKeyFactory.fromAccountId(SOURCE_ACCOUNT_ID) 14 | val NETWORK = NetworkParams("Example Test Network") 15 | val SIMPLE_OP = ManageBalanceOp( 16 | ManageBalanceAction.CREATE, 17 | SOURCE_ACCOUNT_PUBKEY, 18 | "OLG", 19 | ManageBalanceOp.ManageBalanceOpExt.EmptyVersion() 20 | ) 21 | val SIMPLE_OP_2 = ManageBalanceOp( 22 | ManageBalanceAction.CREATE, 23 | SOURCE_ACCOUNT_PUBKEY, 24 | "OLE", 25 | ManageBalanceOp.ManageBalanceOpExt.EmptyVersion() 26 | ) 27 | 28 | @Test 29 | fun singleOperation() { 30 | val operationBody = Operation.OperationBody.ManageBalance(SIMPLE_OP) 31 | val transaction = TransactionBuilder(NETWORK, SOURCE_ACCOUNT_PUBKEY) 32 | .addOperation(operationBody) 33 | .build() 34 | 35 | Assert.assertEquals(SOURCE_ACCOUNT_PUBKEY, transaction.sourceAccountId) 36 | Assert.assertEquals(NETWORK.passphrase, transaction.networkParams.passphrase) 37 | Assert.assertEquals(operationBody.toBase64(), 38 | transaction.operations[0].body.toBase64()) 39 | } 40 | 41 | @Test 42 | fun multipleOperations() { 43 | val operationBodies = listOf( 44 | Operation.OperationBody.ManageBalance(SIMPLE_OP), 45 | Operation.OperationBody.ManageBalance(SIMPLE_OP_2) 46 | ) 47 | 48 | val transaction = TransactionBuilder(NETWORK, SOURCE_ACCOUNT_PUBKEY) 49 | .addOperations(operationBodies) 50 | .build() 51 | 52 | Assert.assertEquals(SOURCE_ACCOUNT_PUBKEY, transaction.sourceAccountId) 53 | Assert.assertEquals(NETWORK.passphrase, transaction.networkParams.passphrase) 54 | Assert.assertEquals(operationBodies[0].toBase64(), 55 | transaction.operations[0].body.toBase64()) 56 | Assert.assertEquals(operationBodies[1].toBase64(), 57 | transaction.operations[1].body.toBase64()) 58 | } 59 | 60 | @Test 61 | fun setMemo() { 62 | val memoText = "TokenD is awesome" 63 | val transaction = TransactionBuilder(NETWORK, SOURCE_ACCOUNT_PUBKEY) 64 | .addOperation(Operation.OperationBody.ManageBalance(SIMPLE_OP)) 65 | .setMemo(Memo.MemoText(memoText)) 66 | .build() 67 | 68 | Assert.assertTrue(transaction.memo is Memo.MemoText) 69 | Assert.assertEquals(memoText, (transaction.memo as Memo.MemoText).text) 70 | } 71 | 72 | @Test 73 | fun setTimeBounds() { 74 | val timeBounds = TimeBounds(1, 5) 75 | val transaction = TransactionBuilder(NETWORK, SOURCE_ACCOUNT_PUBKEY) 76 | .addOperation(Operation.OperationBody.ManageBalance(SIMPLE_OP)) 77 | .setTimeBounds(timeBounds) 78 | .build() 79 | 80 | Assert.assertEquals(timeBounds.maxTime, transaction.timeBounds.maxTime) 81 | Assert.assertEquals(timeBounds.minTime, transaction.timeBounds.minTime) 82 | } 83 | 84 | @Test 85 | fun setSalt() { 86 | val salt = 42L 87 | val transaction = TransactionBuilder(NETWORK, SOURCE_ACCOUNT_PUBKEY) 88 | .addOperation(Operation.OperationBody.ManageBalance(SIMPLE_OP)) 89 | .setSalt(salt) 90 | .build() 91 | 92 | Assert.assertEquals(salt, transaction.salt) 93 | } 94 | 95 | @Test 96 | fun addSigner() { 97 | val signer = Account.random() 98 | val signatureHint = signer.signDecorated(byteArrayOf()).hint 99 | 100 | val transaction = TransactionBuilder(NETWORK, SOURCE_ACCOUNT_PUBKEY) 101 | .addOperation(Operation.OperationBody.ManageBalance(SIMPLE_OP)) 102 | .addSigner(signer) 103 | .build() 104 | 105 | Assert.assertEquals(1, transaction.signatures.size) 106 | Assert.assertArrayEquals(signatureHint.wrapped, transaction.signatures[0].hint.wrapped) 107 | } 108 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | Please check our [developers guide](https://gitlab.com/tokend/developers-guide) 8 | for further information about branching and tagging conventions. 9 | 10 | ## [3.7.0] 2021-11-25 11 | 12 | ### Added 13 | - `Destroyable` implementation in `Account` 14 | - `equal` and `hashCode` methods for `Account` 15 | 16 | ### Changed 17 | - From now `Account` is always a complete keypair with a private key. 18 | To verify signatures use `Account.verifySignature` static methods 19 | - Updated XDR version to `7708446fd03153bf0d99b299c9df054dd9788c7c` 20 | - Updated Kotlin version to 1.4.10 21 | 22 | ### Fixed 23 | - Inability to convert true `UInt64` amounts from and to precised 24 | 25 | ### Removed 26 | - Positive number restriction on transaction salt 27 | - `canSign` method from account (see above) 28 | - `Account.fromAccountId` and `Account.fromPublicKey` methods (see above) 29 | 30 | ## [3.6.5] 2020-08-12 31 | 32 | ### Changed 33 | - Updated XDR version to `d639694` 34 | 35 | ## [3.6.4] 2020-07-15 36 | 37 | ### Changed 38 | - XDR strings are now encoded end decoded in UTF-8 39 | 40 | ## [3.6.3] 2020-01-20 41 | 42 | ### Added 43 | - `Serializable` marker to `NetworkParams` 44 | 45 | ## [3.6.2] 2019-12-25 46 | 47 | ### Fixed 48 | - Incorrect salt transform in `Transaction` constructor, use 49 | true absolute value now 50 | 51 | ## [3.6.1] 2019-11-26 52 | 53 | ### Added 54 | - `Transaction` constructor from XDR `TransactionEnvelope` 55 | 56 | ### Changed 57 | - Signing-related `Transaction` methods are now static 58 | 59 | ## [3.6.0] 2019-11-05 60 | 61 | ### Changed 62 | - Updated XDR version to `78afc23` 63 | 64 | ## [3.5.0] 2019-09-24 65 | 66 | ### Changed 67 | - Updated XDR version to `bfc2e7b` 68 | - Updated TokenD Maven repo domain 69 | 70 | ## [3.4.2] 2019-09-06 71 | 72 | ### Fixed 73 | - XDR decoding on devices running Java 7 74 | 75 | ### Changed 76 | - ProGuard rules 77 | 78 | ## [3.4.1] 2019-09-05 79 | 80 | ### Changed 81 | - ProGuard rules 82 | 83 | ## [3.4.0] 2019-09-04 84 | 85 | ### Added 86 | - Ability to decode XDR models: call `.fromXdr` or `.fromBase64` methods 87 | of the required class in Kotlin or use `*.Decoder` static member in Java 88 | - ProGuard rules 89 | - Ability to add a collection of operation bodies to 90 | `TransactionBuilder` 91 | 92 | ### Removed 93 | - Apache encoding libraries 94 | 95 | ### Changed 96 | - Updated XDR version to `9199f20` 97 | 98 | ## [3.3.0] 2019-07-15 99 | 100 | ### Added 101 | - Ability to add `DecoratedSignature` to the transaction directly 102 | 103 | ## [3.2.0] 2019-06-18 104 | 105 | ### Changed 106 | - Updated XDR version to `cd889b0` 107 | 108 | ## [3.1.0] 2019-05-27 109 | 110 | ### Added 111 | - Ability to add transaction signers with the `TransactionBuilder` 112 | 113 | ### Changed 114 | - Updated way of XDR generation, check out Readme 115 | - Updated XDR version to `3.3.0` (`c8561fd`) 116 | 117 | ## [3.0.1] 2019-02-28 118 | 119 | ### Added 120 | - `reference` optional param for `SimplePaymentOp` 121 | - Account rules and roles 122 | - Signer rules and roles 123 | 124 | ### Changed 125 | - XDR version to `7e06563` 126 | - `PaymentV2` and all related `-V2` things to just `Payment` 127 | 128 | ### Fixed 129 | - Wrong value type for `KeyValueEntryValue` 130 | 131 | [3.0.1]: https://github.com/tokend/kotlin-wallet/compare/1.0.13...3.0.1 132 | [3.1.0]: https://github.com/tokend/kotlin-wallet/compare/3.0.1...3.1.0 133 | [3.2.0]: https://github.com/tokend/kotlin-wallet/compare/3.1.0...3.2.0 134 | [3.3.0]: https://github.com/tokend/kotlin-wallet/compare/3.2.0...3.3.0 135 | [3.4.0]: https://github.com/tokend/kotlin-wallet/compare/3.3.0...3.4.0 136 | [3.4.1]: https://github.com/tokend/kotlin-wallet/compare/3.4.0...3.4.1 137 | [3.4.2]: https://github.com/tokend/kotlin-wallet/compare/3.4.1...3.4.2 138 | [3.5.0]: https://github.com/tokend/kotlin-wallet/compare/3.4.2...3.5.0 139 | [3.6.0]: https://github.com/tokend/kotlin-wallet/compare/3.5.0...3.6.0 140 | [3.6.1]: https://github.com/tokend/kotlin-wallet/compare/3.6.0...3.6.1 141 | [3.6.2]: https://github.com/tokend/kotlin-wallet/compare/3.6.1...3.6.2 142 | [3.6.3]: https://github.com/tokend/kotlin-wallet/compare/3.6.2...3.6.3 143 | [3.6.4]: https://github.com/tokend/kotlin-wallet/compare/3.6.3...3.6.4 144 | [3.6.5]: https://github.com/tokend/kotlin-wallet/compare/3.6.4...3.6.5 145 | [3.7.0]: https://github.com/tokend/kotlin-wallet/compare/3.6.5...3.7.0 146 | [Unreleased]: https://github.com/tokend/kotlin-wallet/compare/3.7.0...HEAD 147 | -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/AccountTest.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import com.google.common.io.BaseEncoding 4 | import org.junit.Assert 5 | import org.junit.Test 6 | import org.tokend.wallet.Account 7 | import org.tokend.wallet.Base32Check 8 | import org.tokend.wallet.xdr.PublicKey 9 | 10 | class AccountTest { 11 | private val SEED = "SBUFJEEK7FMWXPE4HGOWQZPHZ4V5TFKGSF664RAGT24NS662MKTQ7J6S" 12 | private val ACCOUNT_ID = "GB6ZRRKDAHUFQAGSJWMLCXL4W7OIEQNJL4NOISQUA6G23WK3OR3MGC4L" 13 | private val XDR_PUBLIC_KEY = "AAAAAH2YxUMB6FgA0k2YsV18t9yCQalfGuRKFAeNrdlbdHbD" 14 | private val DATA = "TokenD is awesome".toByteArray() 15 | 16 | @Test 17 | fun sign() { 18 | val expectedSig = "1B0EBBAE618B267668A8122ECCCD2A20480BC81951EB401E0F92B613483B798763D36AEB4B0404BC2A31FA1EAD47522BBA08705AB51BA205020E67D09AE87D0E" 19 | val account = Account.fromSecretSeed(SEED.toCharArray()) 20 | val sig = account.sign(DATA) 21 | Assert.assertArrayEquals(BaseEncoding.base16().decode(expectedSig), sig) 22 | } 23 | 24 | @Test 25 | fun verifyValid() { 26 | val sig = "1B0EBBAE618B267668A8122ECCCD2A20480BC81951EB401E0F92B613483B798763D36AEB4B0404BC2A31FA1EAD47522BBA08705AB51BA205020E67D09AE87D0E" 27 | Assert.assertTrue(Account.verifySignature(DATA, BaseEncoding.base16().decode(sig), ACCOUNT_ID)) 28 | } 29 | 30 | @Test 31 | fun verifyValidPublicKey() { 32 | val sig = "1B0EBBAE618B267668A8122ECCCD2A20480BC81951EB401E0F92B613483B798763D36AEB4B0404BC2A31FA1EAD47522BBA08705AB51BA205020E67D09AE87D0E" 33 | val publicKey = PublicKey.Decoder.fromBase64(XDR_PUBLIC_KEY) 34 | Assert.assertTrue(Account.verifySignature(DATA, BaseEncoding.base16().decode(sig), publicKey)) 35 | } 36 | 37 | @Test 38 | fun verifyInvalid() { 39 | val account = Account.fromSecretSeed(SEED.toCharArray()) 40 | Assert.assertFalse(account.verifySignature(ByteArray(0), ByteArray(0))) 41 | } 42 | 43 | @Test 44 | fun fromSeedString() { 45 | val account = Account.fromSecretSeed(SEED.toCharArray()) 46 | Assert.assertEquals(SEED, String(account.secretSeed)) 47 | } 48 | 49 | @Test 50 | fun fromSeedBytes() { 51 | val seed = (0 until 32).map { it.toByte() }.toByteArray() 52 | val account = Account.fromSecretSeed(seed) 53 | Assert.assertEquals(String(Base32Check.encodeSecretSeed(seed)), String(account.secretSeed)) 54 | } 55 | 56 | @Test() 57 | fun random() { 58 | val first = Account.random() 59 | val second = Account.random() 60 | Assert.assertNotEquals(first.secretSeed, second.secretSeed) 61 | } 62 | 63 | @Test 64 | fun accountId() { 65 | val account = Account.fromSecretSeed(SEED.toCharArray()) 66 | Assert.assertEquals(ACCOUNT_ID, account.accountId) 67 | } 68 | 69 | @Test 70 | fun xdrPublicKey() { 71 | val account = Account.fromSecretSeed(SEED.toCharArray()) 72 | Assert.assertEquals(XDR_PUBLIC_KEY, account.xdrPublicKey.toBase64()) 73 | Assert.assertEquals(ACCOUNT_ID, account.accountId) 74 | Assert.assertArrayEquals(Base32Check.decodeAccountId(ACCOUNT_ID), (account.xdrPublicKey as PublicKey.KeyTypeEd25519).ed25519.wrapped) 75 | } 76 | 77 | @Test 78 | fun signDecorated() { 79 | val account = Account.fromSecretSeed(SEED.toCharArray()) 80 | val expectedSig = "W3R2wwAAAEAbDruuYYsmdmioEi7MzSogSAvIGVHrQB4PkrYTSDt5h2PTautLBAS8KjH6Hq1HUiu6CHBatRuiBQIOZ9Ca6H0O" 81 | val decoratedSignature = account.signDecorated(DATA) 82 | Assert.assertEquals(expectedSig, decoratedSignature.toBase64()) 83 | } 84 | 85 | @Test 86 | fun destroy() { 87 | val account = Account.fromSecretSeed(SEED.toCharArray()) 88 | account.destroy() 89 | Assert.assertTrue(account.isDestroyed) 90 | Assert.assertFalse(account.secretSeed.any { it != '0' }) 91 | } 92 | 93 | @Test 94 | fun equals() { 95 | val accountA = Account.fromSecretSeed(SEED.toCharArray()) 96 | val accountB = Account.fromSecretSeed(SEED.toCharArray()) 97 | 98 | Assert.assertEquals(accountA, accountB) 99 | Assert.assertEquals(accountB, accountA) 100 | 101 | val accountC = Account.random() 102 | 103 | Assert.assertNotEquals(accountA, accountC) 104 | Assert.assertNotEquals(accountB, accountC) 105 | 106 | accountB.destroy() 107 | 108 | Assert.assertNotEquals(accountA, accountB) 109 | Assert.assertNotEquals(accountB, accountA) 110 | } 111 | 112 | @Test 113 | fun hashCodee() { 114 | val accountA = Account.fromSecretSeed(SEED.toCharArray()) 115 | val accountB = Account.fromSecretSeed(SEED.toCharArray()) 116 | 117 | Assert.assertEquals(accountA.hashCode(), accountB.hashCode()) 118 | 119 | accountB.destroy() 120 | 121 | Assert.assertNotEquals(accountA.hashCode(), accountB.hashCode()) 122 | } 123 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/Account.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet 2 | 3 | import org.tokend.crypto.ecdsa.Curves 4 | import org.tokend.crypto.ecdsa.EcDSAKeyPair 5 | import org.tokend.crypto.ecdsa.erase 6 | import org.tokend.wallet.xdr.DecoratedSignature 7 | import org.tokend.wallet.xdr.PublicKey 8 | import org.tokend.wallet.xdr.SignatureHint 9 | import org.tokend.wallet.xdr.Uint256 10 | import javax.security.auth.Destroyable 11 | 12 | /** 13 | * Represents TokenD account defined by EcDSA private and/or public keys. 14 | * In this case account is a synonym of keypair. 15 | * 16 | * @see ECDSA 17 | * @see Ed25519 18 | */ 19 | class Account(private val ecDSAKeyPair: EcDSAKeyPair) : Destroyable { 20 | /** 21 | * @return private key seed encoded by [Base32Check]. 22 | * 23 | * @see Secret seed in the Knowledge base 24 | */ 25 | val secretSeed: CharArray = ecDSAKeyPair.privateKeySeed.let(Base32Check::encodeSecretSeed) 26 | 27 | /** 28 | * @return public key bytes. 29 | */ 30 | val publicKey: ByteArray = ecDSAKeyPair.publicKeyBytes 31 | 32 | /** 33 | * @return public key encoded by [Base32Check]. 34 | * 35 | * @see AccountID in the Knowledge base 36 | */ 37 | val accountId: String = Base32Check.encodeAccountId(publicKey) 38 | 39 | /** 40 | * @return public key wrapped into XDR. 41 | */ 42 | val xdrPublicKey: PublicKey 43 | get() = PublicKey.KeyTypeEd25519(Uint256(publicKey)) 44 | 45 | /** 46 | * Signs provided data with the account's private key. 47 | */ 48 | fun sign(data: ByteArray): ByteArray { 49 | return ecDSAKeyPair.sign(data) 50 | } 51 | 52 | /** 53 | * Verifies provided data and signature with the account's public key. 54 | */ 55 | fun verifySignature(data: ByteArray, signature: ByteArray): Boolean { 56 | return ecDSAKeyPair.verify(data, signature) 57 | } 58 | 59 | /** 60 | * Signs provided data with the account's private key 61 | * and wraps the signature into XDR. 62 | * 63 | * @see sign 64 | */ 65 | fun signDecorated(data: ByteArray): DecoratedSignature { 66 | return DecoratedSignature(getSignatureHint(), sign(data)) 67 | } 68 | 69 | private fun getSignatureHint(): SignatureHint { 70 | val signatureHintBytes = publicKey.copyOfRange(publicKey.size - 4, publicKey.size) 71 | 72 | return SignatureHint(signatureHintBytes) 73 | } 74 | 75 | override fun destroy() { 76 | secretSeed.erase() 77 | ecDSAKeyPair.destroy() 78 | } 79 | 80 | override fun isDestroyed(): Boolean { 81 | return ecDSAKeyPair.isDestroyed 82 | } 83 | 84 | override fun equals(other: Any?): Boolean { 85 | if (this === other) return true 86 | if (javaClass != other?.javaClass) return false 87 | 88 | other as Account 89 | 90 | if (!publicKey.contentEquals(other.publicKey)) return false 91 | if (isDestroyed != other.isDestroyed) return false 92 | 93 | return true 94 | } 95 | 96 | override fun hashCode(): Int { 97 | var result = publicKey.contentHashCode() 98 | result = 31 * result + isDestroyed.hashCode() 99 | return result 100 | } 101 | 102 | 103 | companion object { 104 | private const val CURVE = Curves.ED25519 105 | 106 | /** 107 | * Creates an account from a secret seed. 108 | * 109 | * @param seed [Base32Check] encoded private key seed. Will be decoded and duplicated 110 | * so can be erased after account creation. 111 | * 112 | * @see Base32Check 113 | * @see Account.secretSeed 114 | */ 115 | @JvmStatic 116 | fun fromSecretSeed(seed: CharArray): Account { 117 | val decoded = Base32Check.decodeSecretSeed(seed) 118 | val keypair = fromSecretSeed(decoded) 119 | decoded.erase() 120 | return keypair 121 | } 122 | 123 | /** 124 | * Creates an account from a raw 32 byte secret seed. 125 | * 126 | * @param seed 32 bytes of the private key seed. Will be duplicated. 127 | */ 128 | @JvmStatic 129 | fun fromSecretSeed(seed: ByteArray): Account { 130 | return Account(EcDSAKeyPair.fromPrivateKeySeed(CURVE, seed)) 131 | } 132 | 133 | /** 134 | * Creates an account from a random private key. 135 | */ 136 | @JvmStatic 137 | fun random(): Account { 138 | return Account(EcDSAKeyPair.random(CURVE)) 139 | } 140 | 141 | /** 142 | * Verifies [signature] for provided [data] with public key decoded from [accountId] 143 | */ 144 | @JvmStatic 145 | fun verifySignature(data: ByteArray, 146 | signature: ByteArray, 147 | accountId: String): Boolean = 148 | verifySignature(data, signature, PublicKeyFactory.fromAccountId(accountId)) 149 | 150 | /** 151 | * Verifies [signature] for provided [data] with [publicKey] 152 | */ 153 | @JvmStatic 154 | fun verifySignature(data: ByteArray, 155 | signature: ByteArray, 156 | publicKey: ByteArray): Boolean = 157 | verifySignature(data, signature, PublicKey.KeyTypeEd25519(Uint256(publicKey))) 158 | 159 | /** 160 | * Verifies [signature] for provided [data] with [xdrPublicKey] 161 | */ 162 | @JvmStatic 163 | fun verifySignature(data: ByteArray, 164 | signature: ByteArray, 165 | xdrPublicKey: PublicKey): Boolean { 166 | require(xdrPublicKey is PublicKey.KeyTypeEd25519) { 167 | "Only Ed25519 keys are supported" 168 | } 169 | 170 | return EcDSAKeyPair.verify(CURVE, data, signature, xdrPublicKey.ed25519.wrapped) 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/xdr/utils/ReflectiveXdrDecoder.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet.xdr.utils 2 | 3 | import org.tokend.wallet.xdr.XdrByteArrayFixed16 4 | import org.tokend.wallet.xdr.XdrByteArrayFixed32 5 | import org.tokend.wallet.xdr.XdrByteArrayFixed4 6 | import java.lang.reflect.Modifier 7 | 8 | /** 9 | * Used to decode XDRs with reflection. 10 | */ 11 | object ReflectiveXdrDecoder { 12 | /** 13 | * @return [ReflectiveXdrDecoder]-based [XdrDecodable] instance for given type 14 | */ 15 | inline fun wrapType(): XdrDecodable { 16 | return object : XdrDecodable { 17 | override fun fromXdr(stream: XdrDataInputStream): T { 18 | return read(T::class.java, stream) 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * @return Value of [clazz] type decoded from [stream] content 25 | */ 26 | @JvmStatic 27 | @Suppress("UNCHECKED_CAST") 28 | fun read(clazz: Class, stream: XdrDataInputStream): T { 29 | return when { 30 | isPrimitive(clazz) -> readPrimitive(clazz, stream) 31 | isEnum(clazz) -> readEnum(clazz, stream) 32 | isFixedByteArray(clazz) -> readFixedByteArray(clazz, stream) 33 | isUnionSwitch(clazz) -> readUnionSwitch(clazz, stream) 34 | isArray(clazz) -> readArray(clazz, stream) 35 | else -> readComplex(clazz, stream) 36 | } as T 37 | } 38 | 39 | // region Primitives 40 | private val primitives = setOf("int", "long", "boolean", "java.lang.String", "[B") 41 | 42 | private fun isPrimitive(type: Class): Boolean { 43 | return type.name in primitives 44 | } 45 | 46 | private fun readPrimitive(type: Class, stream: XdrDataInputStream): Any { 47 | return when (type.name) { 48 | "int" -> Int.fromXdr(stream) 49 | "long" -> Long.fromXdr(stream) 50 | "boolean" -> Boolean.fromXdr(stream) 51 | "java.lang.String" -> String.fromXdr(stream) 52 | "[B" -> XdrOpaque.fromXdr(stream) 53 | else -> error("Unknown primitive $type") 54 | } 55 | } 56 | // endregion 57 | 58 | // region Fixed byte arrays 59 | private fun isFixedByteArray(type: Class): Boolean { 60 | return XdrFixedByteArray::class.java.isAssignableFrom(type) 61 | } 62 | 63 | private fun readFixedByteArray(type: Class, stream: XdrDataInputStream): 64 | XdrFixedByteArray { 65 | val readByteArray = { size: Int -> 66 | ByteArray(size).also { stream.read(it) } 67 | } 68 | 69 | return when (type) { 70 | XdrByteArrayFixed4::class.java -> XdrByteArrayFixed4(readByteArray(4)) 71 | XdrByteArrayFixed16::class.java -> XdrByteArrayFixed16(readByteArray(16)) 72 | XdrByteArrayFixed32::class.java -> XdrByteArrayFixed32(readByteArray(32)) 73 | else -> error("Unknown fixed byte array $type") 74 | } 75 | } 76 | // endregion 77 | 78 | // region Union switch 79 | private fun getUnionSwitchDiscriminantType(type: Class): Class<*>? { 80 | val constructor = type.declaredConstructors[0] 81 | val paramAnnotations = constructor.parameterAnnotations 82 | val paramTypes = constructor.parameterTypes 83 | 84 | paramAnnotations.forEachIndexed { i, annotations -> 85 | if (annotations.any { it is XdrDiscriminantField }) 86 | return paramTypes[i] 87 | } 88 | 89 | return null 90 | } 91 | 92 | private fun isUnionSwitch(type: Class): Boolean { 93 | return !type.isArray && Modifier.isAbstract(type.modifiers) 94 | && getUnionSwitchDiscriminantType(type) != null 95 | } 96 | 97 | private fun readUnionSwitch(type: Class, stream: XdrDataInputStream): Any { 98 | val discriminantEnumType = getUnionSwitchDiscriminantType(type)!! 99 | val discriminantEnumValue = readEnum(discriminantEnumType, stream) 100 | 101 | val nameKey = discriminantEnumValue.toString() 102 | .toLowerCase() 103 | .replace("_", "") 104 | 105 | val armClass = type.declaredClasses 106 | .find { 107 | it.simpleName.toLowerCase() == nameKey 108 | } 109 | ?: error("Unknown union switch $type arm index $discriminantEnumValue") 110 | 111 | return readComplex(armClass, stream) 112 | } 113 | // endregion 114 | 115 | // region Enum 116 | private fun isEnum(type: Class): Boolean { 117 | return type.isEnum 118 | } 119 | 120 | private fun readEnum(type: Class, stream: XdrDataInputStream): Any { 121 | val value = Int.fromXdr(stream) 122 | val values = type.enumConstants 123 | 124 | val valueField = type.getDeclaredField("value") 125 | 126 | valueField?.isAccessible = true 127 | 128 | val found = values.find { 129 | valueField?.get(it) == value 130 | } 131 | 132 | valueField?.isAccessible = false 133 | 134 | return found ?: error("Can't find ${type.name} enum value for $value") 135 | } 136 | // endregion 137 | 138 | // region Complex 139 | private fun readComplex(type: Class, stream: XdrDataInputStream): Any { 140 | val constructor = type.declaredConstructors[0] 141 | val paramTypes = constructor.parameterTypes 142 | val paramAnnotations = constructor.parameterAnnotations 143 | 144 | val args = paramTypes.mapIndexed { i, paramType -> 145 | val isOptional = paramAnnotations[i].any { annotation -> 146 | annotation is XdrOptionalField 147 | } 148 | 149 | // Read value if it's not optional or is optional and present. 150 | if (!isOptional || Boolean.fromXdr(stream)) { 151 | read(paramType, stream) 152 | } else { 153 | null 154 | } 155 | } 156 | 157 | return constructor.newInstance(*args.toTypedArray()) 158 | } 159 | // endregion 160 | 161 | // region Arrays 162 | private fun isArray(type: Class): Boolean { 163 | return type.isArray 164 | } 165 | 166 | private fun readArray(type: Class, stream: XdrDataInputStream): Any { 167 | val elementType = type.componentType 168 | val size = Int.fromXdr(stream) 169 | val array = java.lang.reflect.Array.newInstance(elementType, size) 170 | for (i in 0 until size) { 171 | java.lang.reflect.Array.set(array, i, read(elementType, stream)) 172 | } 173 | return array 174 | } 175 | // endregion 176 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/Base32Check.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet 2 | 3 | import org.tokend.crypto.ecdsa.erase 4 | import org.tokend.wallet.utils.SecureBase32 5 | import java.io.ByteArrayOutputStream 6 | import java.io.IOException 7 | import java.util.* 8 | 9 | /** 10 | * Performs encoding and decoding of specific data to Base32Check. 11 | * Base32Check is Base32 encoding with version byte and checksum: 12 | * [version byte] + [data] + [CRC16 checksum of version byte and data] 13 | */ 14 | object Base32Check { 15 | /** 16 | * Indicates that there was a problem decoding base32-checked encoded string. 17 | */ 18 | class FormatException(message: String) : RuntimeException(message) 19 | 20 | enum class VersionByte constructor(private val value: Byte) { 21 | ACCOUNT_ID(48.toByte()), // G 22 | SEED(144.toByte()), // S 23 | BALANCE_ID(8.toByte()); // B 24 | 25 | fun getValue(): Int { 26 | return value.toInt() 27 | } 28 | 29 | companion object { 30 | @JvmStatic 31 | fun valueOf(value: Byte): VersionByte? { 32 | return when (value) { 33 | ACCOUNT_ID.value -> ACCOUNT_ID 34 | SEED.value -> SEED 35 | BALANCE_ID.value -> BALANCE_ID 36 | else -> null 37 | } 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * @return [true] if encoded data is related to the given version byte 44 | * and can be decoded, [false] otherwise. 45 | */ 46 | @JvmStatic 47 | fun isValid(versionByte: VersionByte, data: CharArray): Boolean { 48 | try { 49 | decodeCheck(versionByte, data) 50 | return true 51 | } catch (_: Exception) { 52 | return false 53 | } 54 | } 55 | 56 | /** 57 | * Encodes given data using [VersionByte.ACCOUNT_ID] version byte. 58 | */ 59 | @JvmStatic 60 | fun encodeAccountId(data: ByteArray): String { 61 | return String(encodeCheck(VersionByte.ACCOUNT_ID, data)) 62 | } 63 | 64 | /** 65 | * Decodes given data using [VersionByte.ACCOUNT_ID] version byte. 66 | */ 67 | @JvmStatic 68 | fun decodeAccountId(data: String): ByteArray { 69 | return decodeCheck(VersionByte.ACCOUNT_ID, data.toCharArray()) 70 | } 71 | 72 | /** 73 | * Encodes given data using [VersionByte.SEED] version byte. 74 | */ 75 | @JvmStatic 76 | fun encodeSecretSeed(data: ByteArray): CharArray { 77 | return encodeCheck(VersionByte.SEED, data) 78 | } 79 | 80 | /** 81 | * Decodes given data using [VersionByte.SEED] version byte. 82 | */ 83 | @JvmStatic 84 | fun decodeSecretSeed(data: CharArray): ByteArray { 85 | return decodeCheck(VersionByte.SEED, data) 86 | } 87 | 88 | /** 89 | * Encodes given data using [VersionByte.BALANCE_ID] version byte. 90 | */ 91 | @JvmStatic 92 | fun encodeBalanceId(data: ByteArray): String { 93 | return String(encodeCheck(VersionByte.BALANCE_ID, data)) 94 | } 95 | 96 | /** 97 | * Decodes given data using [VersionByte.BALANCE_ID] version byte. 98 | */ 99 | @JvmStatic 100 | fun decodeBalanceId(data: String): ByteArray { 101 | return decodeCheck(VersionByte.BALANCE_ID, data.toCharArray()) 102 | } 103 | 104 | /** 105 | * Encodes given data using given version byte. 106 | */ 107 | @JvmStatic 108 | fun encodeCheck(versionByte: VersionByte, data: ByteArray): CharArray { 109 | try { 110 | val outputStream = ByteArrayOutputStream() 111 | outputStream.write(versionByte.getValue()) 112 | outputStream.write(data) 113 | val payload = outputStream.toByteArray() 114 | val checksum = calculateChecksum(payload) 115 | outputStream.write(checksum) 116 | val unencoded = outputStream.toByteArray() 117 | 118 | val charsEncoded = SecureBase32.encode(unencoded) 119 | 120 | if (VersionByte.SEED == versionByte) { 121 | unencoded.erase() 122 | payload.erase() 123 | checksum.erase() 124 | } 125 | 126 | return charsEncoded 127 | } catch (e: IOException) { 128 | throw AssertionError(e) 129 | } 130 | 131 | } 132 | 133 | /** 134 | * Decodes given data using given version byte. 135 | */ 136 | @JvmStatic 137 | fun decodeCheck(versionByte: VersionByte, encoded: CharArray): ByteArray { 138 | val decodingResult = decode(encoded) 139 | 140 | if (versionByte != decodingResult.first) 141 | throw FormatException("Version byte is invalid") 142 | 143 | return decodingResult.second 144 | } 145 | 146 | /** 147 | * Decodes given data and obtains it's version byte. 148 | * 149 | * @return [Pair] of the version byte and decoded data 150 | */ 151 | @JvmStatic 152 | fun decode(encoded: CharArray): Pair { 153 | val bytes = ByteArray(encoded.size) 154 | for (i in encoded.indices) { 155 | if (encoded[i].toInt() > 127) { 156 | throw IllegalArgumentException("Illegal characters in encoded char array.") 157 | } 158 | bytes[i] = encoded[i].toByte() 159 | } 160 | 161 | val decoded = SecureBase32.decode(encoded) 162 | val decodedVersionByte = decoded[0] 163 | val payload = Arrays.copyOfRange(decoded, 0, decoded.size - 2) 164 | val data = Arrays.copyOfRange(payload, 1, payload.size) 165 | val checksum = Arrays.copyOfRange(decoded, decoded.size - 2, decoded.size) 166 | 167 | val expectedChecksum = calculateChecksum(payload) 168 | 169 | if (!Arrays.equals(expectedChecksum, checksum)) { 170 | throw FormatException("Checksum invalid") 171 | } 172 | 173 | if (VersionByte.SEED.getValue() == decodedVersionByte.toInt()) { 174 | Arrays.fill(bytes, 0.toByte()) 175 | Arrays.fill(decoded, 0.toByte()) 176 | Arrays.fill(payload, 0.toByte()) 177 | } 178 | 179 | val versionByte = VersionByte.valueOf(decodedVersionByte) 180 | ?: throw FormatException("Version byte is invalid") 181 | 182 | return Pair(versionByte, data) 183 | } 184 | 185 | @JvmStatic 186 | private fun calculateChecksum(bytes: ByteArray): ByteArray { 187 | // This code calculates CRC16-XModem checksum 188 | // Ported from https://github.com/alexgorbatchev/node-crc 189 | var crc = 0x0000 190 | var count = bytes.size 191 | var i = 0 192 | var code: Int 193 | 194 | while (count > 0) { 195 | code = crc.ushr(8) and 0xFF 196 | code = code xor (bytes[i++].toInt() and 0xFF) 197 | code = code xor code.ushr(4) 198 | crc = crc shl 8 and 0xFFFF 199 | crc = crc xor code 200 | code = code shl 5 and 0xFFFF 201 | crc = crc xor code 202 | code = code shl 7 and 0xFFFF 203 | crc = crc xor code 204 | count-- 205 | } 206 | 207 | // little-endian 208 | return byteArrayOf(crc.toByte(), crc.ushr(8).toByte()) 209 | } 210 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/tokend/wallet/Transaction.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet 2 | 3 | import org.tokend.wallet.utils.Hashing 4 | import org.tokend.wallet.xdr.* 5 | import org.tokend.wallet.xdr.Transaction 6 | import org.tokend.wallet.xdr.utils.XdrDataOutputStream 7 | import java.io.ByteArrayOutputStream 8 | import java.nio.ByteBuffer 9 | import java.util.* 10 | 11 | /** 12 | * Represents TokenD transaction - a set of operations that changes the state of the system. 13 | * 14 | * @see Transaction in the Knowledge base 15 | */ 16 | class Transaction { 17 | /** 18 | * Params of the network into which the transaction will be sent. 19 | */ 20 | val networkParams: NetworkParams 21 | 22 | /** 23 | * Original account ID of the transaction initiator. 24 | * 25 | * @see Source account ID in the Knowledge base 26 | */ 27 | val sourceAccountId: AccountID 28 | 29 | /** 30 | * Optional transaction payload. 31 | * 32 | * @see Memo in the Knowledge base 33 | */ 34 | val memo: Memo 35 | 36 | /** 37 | * List of operations performed by this transaction. 38 | * 39 | * @see Operations overview in the Knowledge base 40 | */ 41 | val operations: List 42 | 43 | /** 44 | * Time range during which the transaction will be valid. 45 | * 46 | * @see Time bounds in the Knowledge base 47 | */ 48 | val timeBounds: TimeBounds 49 | 50 | /** 51 | * Any number that ensures the uniqueness of the transaction. 52 | */ 53 | val salt: Long 54 | 55 | private val mSignatures = mutableListOf() 56 | 57 | /** 58 | * List of signatures for the transaction. 59 | */ 60 | val signatures: List 61 | get() = mSignatures.toList() 62 | 63 | /** 64 | * Creates unsigned TokenD transaction. 65 | * 66 | * @param networkParams network specification 67 | * @param sourceAccountId account ID of transaction initiator 68 | * @param memo optional transaction payload 69 | * @param timeBounds time range during which the 70 | * transaction will be valid. Default transaction lifetime is [DEFAULT_LIFETIME_SECONDS] 71 | * @param salt optional unique value, random by default, absolute value is taken 72 | * 73 | * @throws IllegalStateException if no operations were added. 74 | */ 75 | @JvmOverloads 76 | constructor(networkParams: NetworkParams, 77 | sourceAccountId: AccountID, 78 | operations: List, 79 | memo: Memo? = null, 80 | timeBounds: TimeBounds? = null, 81 | salt: Long? = null) { 82 | if (operations.isEmpty()) { 83 | throw IllegalStateException("Transaction must contain at least one operation") 84 | } 85 | 86 | this.networkParams = networkParams 87 | this.sourceAccountId = sourceAccountId 88 | this.operations = operations 89 | 90 | this.memo = memo ?: Memo.MemoNone() 91 | this.timeBounds = timeBounds 92 | ?: TimeBounds(0, networkParams.nowTimestamp + DEFAULT_LIFETIME_SECONDS) 93 | this.salt = salt ?: Random().nextLong() 94 | } 95 | 96 | /** 97 | * Creates a copy of given XDR-wrapped transaction. 98 | * 99 | * @param networkParams network specification 100 | * @param transactionEnvelope XDR-wrapped transaction with signatures 101 | */ 102 | constructor(networkParams: NetworkParams, 103 | transactionEnvelope: TransactionEnvelope) { 104 | this.networkParams = networkParams 105 | this.sourceAccountId = transactionEnvelope.tx.sourceAccount 106 | this.operations = transactionEnvelope.tx.operations.toList() 107 | this.memo = transactionEnvelope.tx.memo 108 | this.timeBounds = transactionEnvelope.tx.timeBounds 109 | this.salt = transactionEnvelope.tx.salt 110 | this.mSignatures.addAll(transactionEnvelope.signatures) 111 | } 112 | 113 | /** 114 | * Adds signature from given signer to transaction signatures. 115 | */ 116 | fun addSignature(signer: Account) { 117 | addSignature(getSignature(signer, networkParams.networkId, getXdrTransaction())) 118 | } 119 | 120 | /** 121 | * Adds given signature to transaction signatures. 122 | */ 123 | fun addSignature(decoratedSignature: DecoratedSignature) { 124 | mSignatures.add(decoratedSignature) 125 | } 126 | 127 | /** 128 | * @return SHA-256 hash of the transaction. 129 | */ 130 | fun getHash(): ByteArray { 131 | return Companion.getHash(getSignatureBase(networkParams.networkId, getXdrTransaction())) 132 | } 133 | 134 | /** 135 | * @return XDR-wrapped transaction with all signatures. 136 | */ 137 | fun getEnvelope(): TransactionEnvelope { 138 | return TransactionEnvelope(getXdrTransaction(), mSignatures.toTypedArray()) 139 | } 140 | 141 | private fun getXdrTransaction(): Transaction { 142 | return Transaction( 143 | sourceAccountId, 144 | salt, 145 | timeBounds, 146 | memo, 147 | operations.toTypedArray(), 148 | Transaction.TransactionExt.EmptyVersion() 149 | ) 150 | } 151 | 152 | companion object { 153 | const val DEFAULT_LIFETIME_SECONDS = 7 * 24 * 3600 - 3600L 154 | 155 | /** 156 | * @return [DecoratedSignature] for given transaction by [signer] 157 | */ 158 | @JvmStatic 159 | fun getSignature(signer: Account, 160 | networkId: ByteArray, 161 | xdrTransaction: Transaction): DecoratedSignature { 162 | return signer.signDecorated(getHash(getSignatureBase(networkId, xdrTransaction))) 163 | } 164 | 165 | /** 166 | * @return SHA-256 hash of given transaction signature base 167 | * 168 | * @see getSignatureBase 169 | */ 170 | @JvmStatic 171 | fun getHash(signatureBase: ByteArray): ByteArray { 172 | return Hashing.sha256(signatureBase) 173 | } 174 | 175 | /** 176 | * @return base content for transaction signature 177 | * 178 | * @see NetworkParams.networkId 179 | */ 180 | @JvmStatic 181 | fun getSignatureBase(networkId: ByteArray, 182 | xdrTransaction: Transaction): ByteArray { 183 | val outputStream = ByteArrayOutputStream() 184 | 185 | outputStream.write(networkId) 186 | outputStream.write(ByteBuffer.allocate(4).putInt(EnvelopeType.TX.value).array()) 187 | 188 | val txOutputStream = ByteArrayOutputStream() 189 | val txXdrOutputStream = XdrDataOutputStream(txOutputStream) 190 | xdrTransaction.toXdr(txXdrOutputStream) 191 | outputStream.write(txOutputStream.toByteArray()) 192 | 193 | return outputStream.toByteArray() 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/tokend/wallet_test/DecodingTest.kt: -------------------------------------------------------------------------------- 1 | package org.tokend.wallet_test 2 | 3 | import org.junit.Assert 4 | import org.junit.FixMethodOrder 5 | import org.junit.Test 6 | import org.junit.runners.MethodSorters 7 | import org.tokend.wallet.Base32Check 8 | import org.tokend.wallet.PublicKeyFactory 9 | import org.tokend.wallet.xdr.* 10 | import org.tokend.wallet.xdr.utils.* 11 | import java.io.ByteArrayInputStream 12 | import java.io.ByteArrayOutputStream 13 | 14 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 15 | class DecodingTest { 16 | @Test 17 | fun aDecodeAllRequired() { 18 | val sourceRequest = UpdateMaxIssuance( 19 | assetCode = "OLE", 20 | maxIssuanceAmount = 5495, 21 | ext = UpdateMaxIssuance.UpdateMaxIssuanceExt.EmptyVersion() 22 | ) 23 | 24 | val source = Operation.OperationBody.ManageAsset( 25 | ManageAssetOp( 26 | requestID = 4020, 27 | request = ManageAssetOp.ManageAssetOpRequest.UpdateMaxIssuance( 28 | sourceRequest 29 | ), 30 | ext = ManageAssetOp.ManageAssetOpExt.EmptyVersion() 31 | ) 32 | ) 33 | 34 | val decoded = Operation.OperationBody.fromBase64(source.toBase64()) 35 | 36 | Assert.assertEquals(source.discriminant, decoded.discriminant) 37 | 38 | val decodedOp = (decoded as Operation.OperationBody.ManageAsset).manageAssetOp 39 | 40 | Assert.assertEquals(source.manageAssetOp.requestID, decodedOp.requestID) 41 | Assert.assertEquals(source.manageAssetOp.request.discriminant, decodedOp.request.discriminant) 42 | 43 | val decodedRequest = (decodedOp.request as ManageAssetOp.ManageAssetOpRequest.UpdateMaxIssuance) 44 | .updateMaxIssuance 45 | 46 | Assert.assertEquals(sourceRequest.assetCode, decodedRequest.assetCode) 47 | Assert.assertEquals(sourceRequest.maxIssuanceAmount, decodedRequest.maxIssuanceAmount) 48 | Assert.assertEquals(sourceRequest.ext.discriminant, decodedRequest.ext.discriminant) 49 | } 50 | 51 | @Test 52 | fun bDecodeWithOptionals() { 53 | val source = AccountEntry( 54 | accountID = PublicKeyFactory.fromAccountId( 55 | "GDLWLDE33BN7SG6V4P63V2HFA56JYRMODESBLR2JJ5F3ITNQDUVKS2JE" 56 | ), 57 | roleID = 333, 58 | referrer = null, 59 | sequentialID = 404, 60 | ext = AccountEntry.AccountEntryExt.EmptyVersion() 61 | ) 62 | 63 | val decoded = AccountEntry.fromBase64(source.toBase64()) 64 | 65 | Assert.assertNull(decoded.referrer) 66 | Assert.assertEquals(source.sequentialID, decoded.sequentialID) 67 | } 68 | 69 | @Test 70 | fun cPrimitives() { 71 | 44.also { source -> 72 | Assert.assertEquals(source, ByteArrayOutputStream().let { 73 | source.toXdr(XdrDataOutputStream(it)) 74 | Int.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 75 | }) 76 | } 77 | (-44).also { source -> 78 | Assert.assertEquals(source, ByteArrayOutputStream().let { 79 | source.toXdr(XdrDataOutputStream(it)) 80 | Int.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 81 | }) 82 | } 83 | 84 | Int.MAX_VALUE.also { source -> 85 | Assert.assertEquals(source, ByteArrayOutputStream().let { 86 | source.toXdr(XdrDataOutputStream(it)) 87 | Int.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 88 | }) 89 | } 90 | (Int.MIN_VALUE).also { source -> 91 | Assert.assertEquals(source, ByteArrayOutputStream().let { 92 | source.toXdr(XdrDataOutputStream(it)) 93 | Int.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 94 | }) 95 | } 96 | 97 | 55L.also { source -> 98 | Assert.assertEquals(source, ByteArrayOutputStream().let { 99 | source.toXdr(XdrDataOutputStream(it)) 100 | Long.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 101 | }) 102 | } 103 | (-55L).also { source -> 104 | Assert.assertEquals(source, ByteArrayOutputStream().let { 105 | source.toXdr(XdrDataOutputStream(it)) 106 | Long.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 107 | }) 108 | } 109 | Long.MAX_VALUE.also { source -> 110 | Assert.assertEquals(source, ByteArrayOutputStream().let { 111 | source.toXdr(XdrDataOutputStream(it)) 112 | Long.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 113 | }) 114 | } 115 | (Long.MIN_VALUE).also { source -> 116 | Assert.assertEquals(source, ByteArrayOutputStream().let { 117 | source.toXdr(XdrDataOutputStream(it)) 118 | Long.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 119 | }) 120 | } 121 | 122 | true.also { source -> 123 | Assert.assertEquals(source, ByteArrayOutputStream().let { 124 | source.toXdr(XdrDataOutputStream(it)) 125 | Boolean.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 126 | }) 127 | } 128 | false.also { source -> 129 | Assert.assertEquals(source, ByteArrayOutputStream().let { 130 | source.toXdr(XdrDataOutputStream(it)) 131 | Boolean.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 132 | }) 133 | } 134 | 135 | "TokenD is awesome!".also { source -> 136 | Assert.assertEquals(source, ByteArrayOutputStream().let { 137 | source.toXdr(XdrDataOutputStream(it)) 138 | String.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 139 | }) 140 | } 141 | "".also { source -> 142 | Assert.assertEquals(source, ByteArrayOutputStream().let { 143 | source.toXdr(XdrDataOutputStream(it)) 144 | String.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 145 | }) 146 | } 147 | "¬ Я так соскучился \uD83E\uDD70".also { source -> 148 | Assert.assertEquals(source, ByteArrayOutputStream().let { 149 | source.toXdr(XdrDataOutputStream(it)) 150 | String.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 151 | }) 152 | } 153 | 154 | byteArrayOf(1, 2, 3, 4).also { source -> 155 | Assert.assertArrayEquals(source, ByteArrayOutputStream().let { 156 | source.toXdr(XdrDataOutputStream(it)) 157 | XdrOpaque.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 158 | }) 159 | } 160 | byteArrayOf().also { source -> 161 | Assert.assertArrayEquals(source, ByteArrayOutputStream().let { 162 | source.toXdr(XdrDataOutputStream(it)) 163 | XdrOpaque.fromXdr(XdrDataInputStream(ByteArrayInputStream(it.toByteArray()))) 164 | }) 165 | } 166 | } 167 | 168 | @Test 169 | fun dTxResult() { 170 | val createdBalanceId = "BDGDRIG2WFR7HJESFI35WFUKS5XXEMIZIU44MBXVD3GXNRDXVLFDBGJW" 171 | val result = "AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAJAAAAAAAAAADMOKDasWPzpJIqN9sWipdvcjEZRTnGBvUezXbEd6rKMAAAAAAAAAAA" 172 | val decoded = TransactionResult.fromBase64(result) 173 | Assert.assertEquals( 174 | createdBalanceId, 175 | decoded.result 176 | .let { it as TransactionResult.TransactionResultResult.Txsuccess } 177 | .results 178 | .first() 179 | .let { it as OperationResult.Opinner } 180 | .tr 181 | .let { it as OperationResult.OperationResultTr.ManageBalance } 182 | .manageBalanceResult 183 | .let { it as ManageBalanceResult.Success } 184 | .success 185 | .balanceID 186 | .let { it as PublicKey.KeyTypeEd25519 } 187 | .ed25519 188 | .wrapped 189 | .let(Base32Check::encodeBalanceId) 190 | ) 191 | } 192 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Distributed Lab 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------