Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

newm-chain updates for wallets and nfts #182

Merged
merged 1 commit into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.newm.chain.database.entity

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.math.BigInteger

@Serializable
Expand All @@ -17,4 +18,7 @@ data class LedgerAsset(
// the total supply of this asset
@Contextual
val supply: BigInteger,

@Transient
val txId: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,6 @@ interface LedgerRepository {
fun snapshotNativeAssets(policy: String, name: String): Map<String, Long>

fun createLedgerUtxoHistory(createdUtxos: Set<CreatedUtxo>, blockNumber: Long)

fun queryUsedAddresses(addresses: List<String>): Set<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,16 @@ class LedgerRepositoryImpl : LedgerRepository {
}
}

override fun queryUsedAddresses(addresses: List<String>): Set<String> {
return transaction {
LedgerTable
.slice(LedgerTable.address)
.select { LedgerTable.address inList addresses }
.mapNotNull { row -> row[LedgerTable.address] }
.toHashSet()
}
}

companion object {
private const val ADA_HANDLES_POLICY = "f0ff48bbb7bbe9d59a40f1ce90e9e9d0ff5002ec48f232b49ca0fb9a"
}
Expand Down
3 changes: 3 additions & 0 deletions newm-chain-db/src/main/kotlin/io/newm/chain/util/DbKtx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ fun Block.toLedgerAssets(): List<LedgerAsset> =
policy = asset.policyId,
name = asset.name,
supply = asset.quantity,
txId = txMary.id,
)
}
}
Expand All @@ -468,6 +469,7 @@ fun Block.toLedgerAssets(): List<LedgerAsset> =
policy = asset.policyId,
name = asset.name,
supply = asset.quantity,
txId = txAlonzo.id,
)
}
}
Expand All @@ -478,6 +480,7 @@ fun Block.toLedgerAssets(): List<LedgerAsset> =
policy = asset.policyId,
name = asset.name,
supply = asset.quantity,
txId = txBabbage.id,
)
}
}
Expand Down
24 changes: 24 additions & 0 deletions newm-chain-grpc/src/main/proto/newm_chain.proto
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ service NewmChain {
rpc MonitorNativeAssets (MonitorNativeAssetsRequest) returns (stream MonitorNativeAssetsResponse) {}
rpc CalculateMinUtxoForOutput (OutputUtxo) returns (OutputUtxo) {}
rpc SnapshotNativeAssets (SnapshotNativeAssetsRequest) returns (SnapshotNativeAssetsResponse) {}
rpc DeriveWalletAddresses (DeriveWalletAddressesRequest) returns (DeriveWalletAddressesResponse) {}
}

message SubmitTransactionRequest {
Expand Down Expand Up @@ -278,6 +279,10 @@ message MonitorNativeAssetsResponse {
string native_asset_supply_change = 4;
string native_asset_metadata_json = 5;
}

uint64 slot = 6;
uint64 block = 7;
string tx_hash = 8;
}

message SnapshotNativeAssetsRequest {
Expand All @@ -295,4 +300,23 @@ message SnapshotNativeAssetsResponse {
message SnapshotEntry {
string stake_address = 1;
uint64 amount = 2;
}

message DeriveWalletAddressesRequest {
string wallet_account_xpub_key = 1;
}

message DeriveWalletAddressesResponse {
Address stake_address = 1;
repeated Address enterprise_address = 2;
repeated Address enterprise_change_address = 3;
repeated Address payment_stake_address = 4;
repeated Address payment_stake_change_address = 5;
}

message Address {
string address = 1;
uint32 role = 2;
uint32 index = 3;
bool used = 4;
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,9 @@ object Constants {
const val NETWORK_MAGIC_PREVIEW = 2L
const val NETWORK_MAGIC_PREPROD = 1L

const val ROLE_PAYMENT = 0u
const val ROLE_CHANGE = 1u
const val ROLE_STAKING = 2u

// FIXME: Remove unused stuff
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class BIP32PublicKey(val bech32XPub: String) {
chaincode = xpubBytes.sliceArray(32..63)
}

fun derive(index: Int): BIP32PublicKey {
fun derive(index: UInt): BIP32PublicKey {
val chainCodeKeySpec = SecretKeySpec(chaincode, MAC_ALGORITHM)
val zMac = zMacThreadLocal.getOrSet { Mac.getInstance(MAC_ALGORITHM) }
zMac.init(chainCodeKeySpec)
Expand Down Expand Up @@ -66,7 +66,7 @@ class BIP32PublicKey(val bech32XPub: String) {
cc.copyInto(out, 32, 0, CHAIN_CODE_SIZE)
}

private fun le32(i: Int): ByteArray {
private fun le32(i: UInt): ByteArray {
return byteArrayOf(
i.toByte(),
(i shr 8).toByte(),
Expand Down
20 changes: 19 additions & 1 deletion newm-chain/src/main/kotlin/io/newm/chain/daemon/BlockDaemon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ class BlockDaemon(
transaction {
val ledgerAssets = confirmedBlock.toLedgerAssets().map { ledgerAsset ->
ledgerRepository.queryLedgerAsset(ledgerAsset.policy, ledgerAsset.name)!!
.copy(txId = ledgerAsset.txId)
}
// handle supply changes
val batch = mutableListOf<ByteArray>()
Expand All @@ -690,6 +691,9 @@ class BlockDaemon(
policy = ledgerAsset.policy
name = ledgerAsset.name
nativeAssetSupplyChange = ledgerAsset.supply.toString()
slot = confirmedBlock.header.slot
block = confirmedBlock.header.blockHeight
txHash = ledgerAsset.txId
}.writeTo(bos)
bos.toByteArray()
}
Expand All @@ -700,7 +704,8 @@ class BlockDaemon(
ledgerAssets.filter { it.supply > BigInteger.ZERO }.map { ledgerAsset ->
val metadataLedgerAsset = if (ledgerAsset.name.matches(CIP68_USER_TOKEN_REGEX)) {
val name = "$CIP68_REFERENCE_TOKEN_PREFIX${ledgerAsset.name.substring(8)}"
ledgerRepository.queryLedgerAsset(ledgerAsset.policy, name) ?: run {
ledgerRepository.queryLedgerAsset(ledgerAsset.policy, name)
?.copy(txId = ledgerAsset.txId) ?: run {
log.warn("No LedgerAsset found for: '${ledgerAsset.policy}.$name' !")
ledgerAsset
}
Expand All @@ -718,6 +723,9 @@ class BlockDaemon(
ledgerAsset.policy,
ledgerAsset.name,
)
slot = confirmedBlock.header.slot
block = confirmedBlock.header.blockHeight
txHash = ledgerAsset.txId
}.writeTo(bos)
bos.toByteArray()
}
Expand Down Expand Up @@ -747,6 +755,11 @@ class BlockDaemon(
updatedNativeAsset.policy,
updatedNativeAsset.name
)
slot = confirmedBlock.header.slot
block = confirmedBlock.header.blockHeight
txHash = ledgerAssets.firstOrNull {
it.name == updatedNativeAsset.name && it.policy == updatedNativeAsset.policy
}?.txId ?: ""
}.writeTo(bos)
metadataBatch.add(bos.toByteArray())

Expand All @@ -765,6 +778,11 @@ class BlockDaemon(
nativeAsset.policy,
nativeAsset.name
)
slot = confirmedBlock.header.slot
block = confirmedBlock.header.blockHeight
txHash = ledgerAssets.firstOrNull {
it.name == nativeAsset.name && it.policy == nativeAsset.policy
}?.txId ?: ""
}.writeTo(bos1)
metadataBatch.add(bos1.toByteArray())
}
Expand Down
74 changes: 74 additions & 0 deletions newm-chain/src/main/kotlin/io/newm/chain/grpc/NewmChainService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package io.newm.chain.grpc

import com.google.protobuf.kotlin.toByteString
import io.newm.chain.cardano.address.Address
import io.newm.chain.cardano.address.AddressCredential
import io.newm.chain.cardano.address.BIP32PublicKey
import io.newm.chain.cardano.getCurrentEpoch
import io.newm.chain.config.Config
import io.newm.chain.database.repository.LedgerRepository
import io.newm.chain.ledger.SubmittedTransactionCache
import io.newm.chain.model.toNativeAssetMap
import io.newm.chain.util.Constants.ROLE_CHANGE
import io.newm.chain.util.Constants.ROLE_PAYMENT
import io.newm.chain.util.Constants.ROLE_STAKING
import io.newm.chain.util.hexToByteArray
import io.newm.chain.util.toCreatedUtxoMap
import io.newm.chain.util.toHexString
Expand Down Expand Up @@ -344,4 +350,72 @@ class NewmChainService : NewmChainGrpcKt.NewmChainCoroutineImplBase() {
throw e
}
}

override suspend fun deriveWalletAddresses(request: DeriveWalletAddressesRequest): DeriveWalletAddressesResponse {
try {
// m / purpose' / coin_type' / account' / role / index
// Example: m / 1852' / 1815' / 0' / 0 / 0
// The user has sent us the account public key, so we can derive all roles/index values below that

val rootAccountPk = BIP32PublicKey(bech32XPub = request.walletAccountXpubKey)
val stakePk = rootAccountPk.derive(ROLE_STAKING).derive(0u)
val stakeCredential = AddressCredential.fromKey(stakePk)
val stakeAddress = Address.fromStakeAddressCredential(stakeCredential, Config.isMainnet).address

val paymentRootPk = rootAccountPk.derive(ROLE_PAYMENT)
val enterpriseAddresses = deriveAddresses(ROLE_PAYMENT, paymentRootPk)
val paymentStakeAddresses = deriveAddresses(ROLE_PAYMENT, paymentRootPk, stakeCredential)
val changeRootPk = rootAccountPk.derive(ROLE_CHANGE)
val enterpriseChangeAddresses = deriveAddresses(ROLE_CHANGE, changeRootPk)
val paymentStakeChangeAddresses = deriveAddresses(ROLE_CHANGE, changeRootPk, stakeCredential)

return deriveWalletAddressesResponse {
this.stakeAddress = address {
this.address = stakeAddress
this.role = ROLE_STAKING.toInt()
this.index = 0
this.used = true
}
this.enterpriseAddress.addAll(enterpriseAddresses)
this.paymentStakeAddress.addAll(paymentStakeAddresses)
this.enterpriseChangeAddress.addAll(enterpriseChangeAddresses)
this.paymentStakeChangeAddress.addAll(paymentStakeChangeAddresses)
}
} catch (e: Throwable) {
log.error("deriveWalletAddresses error!", e)
throw e
}
}

private fun deriveAddresses(
role: UInt,
rolePk: BIP32PublicKey,
stakeCredential: AddressCredential? = null
): List<io.newm.chain.grpc.Address> {
val addresses = mutableListOf<io.newm.chain.grpc.Address>()
var indexStart = 0u
do {
val paymentAddresses = (indexStart..indexStart + 39u).map { index ->
val paymentPk = rolePk.derive(index)
val paymentCredential = AddressCredential.fromKey(paymentPk)
stakeCredential?.let {
Address.fromPaymentStakeAddressCredentialsKeyKey(paymentCredential, it, Config.isMainnet).address
} ?: Address.fromPaymentAddressCredential(paymentCredential, Config.isMainnet).address
}
val usedAddresses = ledgerRepository.queryUsedAddresses(paymentAddresses)
addresses.addAll(
paymentAddresses.mapIndexed { index, address ->
address {
this.address = address
this.role = role.toInt()
this.index = (indexStart + index.toUInt()).toInt()
this.used = address in usedAddresses
}
}
)

indexStart += 40u
} while (usedAddresses.isNotEmpty())
return addresses
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ class AddressDerivationTest {

val xpubKey = BIP32PublicKey(xpub)

val pk0 = xpubKey.derive(0).derive(0)
val pk0 = xpubKey.derive(0u).derive(0u)
assertThat(pk0.bech32XPub).isEqualTo("xpub1nnml7qn2un86kgrvqyvs7lmrhz303aprekuaugu9s82elttfnjxu2ltrhxzjvhn6wlt6xn90j2sx6a2ludjpz03xhzsmp7mq476v5usa4meq2")
val pkCredential = AddressCredential.fromKey(pk0)
assertThat(pkCredential.hash.toHexString()).isEqualTo("8e942b2524de8eea1d63fda7e0db33fb3583e5d6d3cb8102c6290771")
val sk0 = xpubKey.derive(2).derive(0)
val sk0 = xpubKey.derive(2u).derive(0u)
assertThat(sk0.bech32XPub).isEqualTo("xpub1hlfscunaww6xd80zyg7cxyqkkf643ywfpnvm9tvpz62900kh2zgglvwup02l0ltxm4xy58tkck8k8quq95wqnnrwnk57n83t8jwmgaqwtlfg8")
val skCredential = AddressCredential.fromKey(sk0)
assertThat(skCredential.hash.toHexString()).isEqualTo("0e54800ff11331412c84a6660d5789e249321a506a5c7fd6521b2487")
Expand Down
24 changes: 24 additions & 0 deletions newm-server/src/test/kotlin/io/newm/server/grpc/GrpcTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.newm.chain.grpc.QueryTransactionConfirmationCountResponse
import io.newm.chain.grpc.QueryUtxosResponse
import io.newm.chain.grpc.SnapshotNativeAssetsResponse
import io.newm.chain.grpc.SubmitTransactionResponse
import io.newm.chain.grpc.deriveWalletAddressesRequest
import io.newm.chain.grpc.monitorAddressRequest
import io.newm.chain.grpc.monitorNativeAssetsRequest
import io.newm.chain.grpc.monitorPaymentAddressRequest
Expand Down Expand Up @@ -417,4 +418,27 @@ class GrpcTests {
assertThat(stakeAddressToAmountMap.size).isEqualTo(4)
assertThat(stakeAddressToAmountMap["total_supply"]).isEqualTo(69L)
}

@Test
@Disabled
fun `test deriveWalletAddresses`() = runBlocking {
// plainText for localhost testing only. use SSL later.
val channel = ManagedChannelBuilder.forAddress("localhost", 3737).usePlaintext().build()
val client =
NewmChainGrpcKt.NewmChainCoroutineStub(channel).withInterceptors(
MetadataUtils.newAttachHeadersInterceptor(
Metadata().apply {
put(
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
"Bearer <JWT_TOKEN_HERE_DO_NOT_COMMIT>"
)
}
)
)
val request = deriveWalletAddressesRequest {
walletAccountXpubKey = "xpub10yq2v72lq0h7lnhkw308uy23fjq384zufvyesh6mlklnpmv048xs8arze4nws0xfp8h87d7jdxwgm5dsr7l0qruedrtcdudjlnxls3sm0qlln"
}
val response = client.deriveWalletAddresses(request)
println("response: $response")
}
}
Loading