Skip to content

Commit

Permalink
add nostr support & fix sdk-kit e2e test (#2673)
Browse files Browse the repository at this point in the history
* finish

* test

* fix
  • Loading branch information
wow-sven committed Sep 24, 2024
1 parent 198ddf0 commit 4466c7d
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 898 deletions.
816 changes: 0 additions & 816 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions sdk/typescript/rooch-sdk-kit/test/case/use-default-client.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

import { useEffect } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { renderHook, waitFor, act } from '@testing-library/react'
import {
ErrorValidateSessionIsExpired,
JsonRpcError,
Expand All @@ -25,7 +28,7 @@ describe('useDefaultClient', () => {

// issue-2481
it('should remove expired session from storage when receives expired error', async () => {
const { mockWallet } = registerMockWallet()
const { mockWallet, unregister } = registerMockWallet()

const mockClient = new RoochClient({ url: 'http://localhost:6767' })
mockClient.signAndExecuteTransaction = vi.fn().mockResolvedValue({
Expand Down Expand Up @@ -89,10 +92,13 @@ describe('useDefaultClient', () => {
return cachedCurrentSession!.getAuthKey() === s.getAuthKey()
}

await expect(result.current.triggerSessionExpiredError).rejects.toThrow(
'[test] session expired',
)
await act(async () => {
expect(result.current.triggerSessionExpiredError).rejects.toThrow('[test] session expired')
})

expect(result.current.sessions).toHaveLength(1)
expect(result.current.sessions.find(getMatchedSessionByAuthKey)).toBeUndefined()

act(() => unregister())
})
})
14 changes: 13 additions & 1 deletion sdk/typescript/rooch-sdk-kit/test/mocks/mock-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '@roochnetwork/rooch-sdk'
import { SupportChain } from '../../src/feature/index.js'
import { Wallet } from '../../src/wellet/wallet.js'
import { Mock } from 'vitest'
import { Mock, vi } from 'vitest'

export class MockBitcoinWallet extends Wallet {
private kp: Keypair
Expand Down Expand Up @@ -99,4 +99,16 @@ export class MockBitcoinWallet extends Wallet {
switchAccount(_: string): void {}

switchNetwork(_: string): void {}

getDescription(): string {
return ''
}

getIcon(theme?: 'dark' | 'light'): string {
return theme ?? ''
}

getInstallUrl(): string {
return ''
}
}
27 changes: 27 additions & 0 deletions sdk/typescript/rooch-sdk/src/address/addressView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

import { Address } from './address.js'
import { BitcoinAddress, BitcoinNetowkType } from './bitcoin.js'
import { NoStrAddress } from './nostr.js'
import { RoochAddress } from './rooch.js'
import { Bytes } from '../types/index.js'

export class AddressView implements Address {
public readonly bitcoinAddress: BitcoinAddress
public readonly noStrAddress: NoStrAddress
public readonly roochAddress: RoochAddress

constructor(publicKey: Bytes, network: BitcoinNetowkType = BitcoinNetowkType.Regtest) {
this.bitcoinAddress = BitcoinAddress.fromPublicKey(publicKey, network)
this.noStrAddress = new NoStrAddress(publicKey)
this.roochAddress = this.bitcoinAddress.genRoochAddress()
}

toBytes(): Uint8Array {
return this.roochAddress.toBytes()
}
toStr(): string {
return this.roochAddress.toStr()
}
}
33 changes: 30 additions & 3 deletions sdk/typescript/rooch-sdk/src/address/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
import { bech32, bech32m } from '@scure/base'

import { bcs } from '../bcs/index.js'
import { Bytes } from '../types/index.js'
import { Bytes, EmptyBytes } from '../types/index.js'
import { blake2b, bytes, isHex, validateWitness } from '../utils/index.js'

import { Address, ROOCH_ADDRESS_LENGTH } from './address.js'
import { ROOCH_ADDRESS_LENGTH } from './address.js'
import { RoochAddress } from './rooch.js'
import { MultiChainID } from './types.js'
import { ThirdPartyAddress } from './thirdparty-address.js'
import { Buffer } from 'buffer'
import bs58check from 'bs58check'
import { schnorr, secp256k1 } from '@noble/curves/secp256k1'

export enum BitcoinNetowkType {
Bitcoin,
Expand Down Expand Up @@ -69,7 +70,7 @@ export class BitcoinNetwork {
}
}

export class BitcoinAddress extends ThirdPartyAddress implements Address {
export class BitcoinAddress extends ThirdPartyAddress {
private readonly bytes: Bytes
private roochAddress: RoochAddress | undefined

Expand Down Expand Up @@ -110,6 +111,32 @@ export class BitcoinAddress extends ThirdPartyAddress implements Address {
}
}

static fromPublicKey(publicKey: Bytes, network: BitcoinNetowkType = BitcoinNetowkType.Regtest) {
const tapTweak = (a: Bytes, b: Bytes) => {
const u = schnorr.utils
const t = u.taggedHash('TapTweak', a, b)
const tn = u.bytesToNumberBE(t)
if (tn >= secp256k1.CURVE.n) throw new Error('tweak higher than curve order')
return tn
}

// Each hex char represents half a byte, hence hex address doubles the length
const u = schnorr.utils
const t = tapTweak(publicKey, EmptyBytes) // t = int_from_bytes(tagged_hash("TapTweak", pubkey + h))
const P = u.lift_x(u.bytesToNumberBE(publicKey)) // P = lift_x(int_from_bytes(pubkey))
const Q = P.add(secp256k1.ProjectivePoint.fromPrivateKey(t)) // Q = point_add(P, point_mul(G, t))
const tweakedPubkey = u.pointToBytes(Q)

// p2tr version with 1
return new BitcoinAddress(
bech32m.encode(
new BitcoinNetwork(network).bech32HRP(),
[1].concat(bech32m.toWords(tweakedPubkey)),
false,
),
)
}

private getPubkeyAddressPrefix(network: BitcoinNetowkType = BitcoinNetowkType.Bitcoin): number {
return network === BitcoinNetowkType.Bitcoin
? PUBKEY_ADDRESS_PREFIX_MAIN
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/rooch-sdk/src/address/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

export * from './address.js'
export * from './addressView.js'
export * from './rooch.js'
export * from './bitcoin.js'
export * from './util.js'
Expand Down
37 changes: 37 additions & 0 deletions sdk/typescript/rooch-sdk/src/address/nostr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

import { bech32 } from '@scure/base'
import { RoochAddress } from './rooch.js'
import { BitcoinAddress } from './bitcoin.js'
import { Bytes } from '../types/bytes.js'

const PREFIX_BECH32_PUBLIC_KEY = 'npub'

export class NoStrAddress {
private readonly str: string
private readonly bytes: Bytes

constructor(input: string | Bytes) {
if (typeof input === 'string') {
this.str = input
this.bytes = bech32.decodeToBytes(input).bytes
return
}

this.bytes = input
this.str = bech32.encode(PREFIX_BECH32_PUBLIC_KEY, bech32.toWords(input), false)
}

genRoochAddress(): RoochAddress {
return BitcoinAddress.fromPublicKey(this.bytes).genRoochAddress()
}

toStr(): string {
return this.str
}

toBytes(): Bytes {
return this.bytes
}
}
2 changes: 1 addition & 1 deletion sdk/typescript/rooch-sdk/src/address/rooch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class RoochAddress implements Address {
if (isHex(address)) {
this.address = fromHEX(address)
} else {
this.address = bech32m.fromWords(bech32m.decode(address as `${string}1${string}`).words)
this.address = bech32m.decodeToBytes(address).bytes
}
} else {
this.address = address
Expand Down
13 changes: 5 additions & 8 deletions sdk/typescript/rooch-sdk/src/address/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,9 @@ export function convertToRoochAddressBytes(input: address): Bytes {
}

if (input.startsWith(ROOCH_BECH32_PREFIX)) {
const decode = bech32m.decode(input as `${string}1${string}`)
const bytes = bech32m.fromWords(decode.words)

if (decode.prefix === ROOCH_BECH32_PREFIX && bytes.length === ROOCH_ADDRESS_LENGTH) {
return bytes
const decode = bech32m.decodeToBytes(input)
if (decode.prefix === ROOCH_BECH32_PREFIX && decode.bytes.length === ROOCH_ADDRESS_LENGTH) {
return decode.bytes
}
}
// throw new Error('invalid address')
Expand Down Expand Up @@ -71,10 +69,9 @@ export function isValidRoochAddress(input: address): input is string {
}

if (input.startsWith(ROOCH_BECH32_PREFIX)) {
const decode = bech32m.decode(input as `${string}1${string}`)
const bytes = bech32m.fromWords(decode.words)
const decode = bech32m.decodeToBytes(input)

return decode.prefix === ROOCH_BECH32_PREFIX && bytes.length === ROOCH_ADDRESS_LENGTH
return decode.prefix === ROOCH_BECH32_PREFIX && decode.bytes.length === ROOCH_ADDRESS_LENGTH
}

return false
Expand Down
3 changes: 1 addition & 2 deletions sdk/typescript/rooch-sdk/src/crypto/publickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

import { Bytes } from '../types/index.js'
import { Address } from '../address/index.js'
import { bytesEqual, toB64 } from '../utils/index.js'

/**
Expand All @@ -13,7 +12,7 @@ export type PublicKeyInitData = string | Bytes | Iterable<number>
/**
* A public key
*/
export abstract class PublicKey<T extends Address> {
export abstract class PublicKey<T> {
/**
* Checks if two public keys are equal
*/
Expand Down
18 changes: 18 additions & 0 deletions sdk/typescript/rooch-sdk/src/keypairs/secp256k1/keypair.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ describe('Secp256k1 keypair', () => {
expect(secret.startsWith(ROOCH_SECRET_KEY_PREFIX)).toBeTruthy()
})

it('Create secp256k1 keypair from CLI secret key', () => {
const testKey = 'roochsecretkey1q969zv4rhqpuj0nkf2e644yppjf34p6zwr3gq0633qc7n9luzg6w6lycezc'
const expectRoochHexAddress =
'0xf892b3fd5fd0e93436ba3dc8d504413769d66901266143d00e49441079243ed0'
const expectRoochBech32Address =
'rooch1lzft8l2l6r5ngd468hyd2pzpxa5av6gpyes585qwf9zpq7fy8mgqh9npj5'
const expectNoStrddress = 'npub1h54r2zvulk96qjmfnyy83mtry0pp5acnz6uvk637typxtvn90c8s0lrc0g'
const expectBitcoinAddress = 'bcrt1pw9l5h7vepq8cnpugwm848x3at34gg5eq0mamdrjw0krunfjm0zfq65gjzz'

const sk = Secp256k1Keypair.fromSecretKey(testKey)
const addrView = sk.getSchnorrPublicKey().toAddress()

expect(addrView.roochAddress.toHexAddress()).eq(expectRoochHexAddress)
expect(addrView.roochAddress.toBech32Address()).eq(expectRoochBech32Address)
expect(addrView.noStrAddress.toStr()).eq(expectNoStrddress)
expect(addrView.bitcoinAddress.toStr()).eq(expectBitcoinAddress)
})

it('Create secp256k1 keypair from secret key', () => {
// valid secret key is provided by rooch keystore
const { sk, pk } = TEST_CASES[0]
Expand Down
4 changes: 2 additions & 2 deletions sdk/typescript/rooch-sdk/src/keypairs/secp256k1/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ export class Secp256k1Keypair extends Keypair {
}

getBitcoinAddress(): BitcoinAddress {
return this.getSchnorrPublicKey().toAddress()
return this.getSchnorrPublicKey().toAddress().bitcoinAddress
}

getRoochAddress(): RoochAddress {
return this.getSchnorrPublicKey().toAddress().genRoochAddress()
return this.getSchnorrPublicKey().toAddress().roochAddress
}

/**
Expand Down
38 changes: 7 additions & 31 deletions sdk/typescript/rooch-sdk/src/keypairs/secp256k1/publickey.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0

import { bech32m } from '@scure/base'
import { schnorr, secp256k1 } from '@noble/curves/secp256k1'
import { secp256k1 } from '@noble/curves/secp256k1'

import { BitcoinAddress, BitcoinNetowkType, BitcoinNetwork } from '../../address/index.js'
import { AddressView, BitcoinNetowkType } from '../../address/index.js'
import { PublicKey, PublicKeyInitData, SIGNATURE_SCHEME_TO_FLAG } from '../../crypto/index.js'
import { Bytes, EmptyBytes } from '../../types/index.js'
import { fromB64, sha256, toHEX } from '../../utils/index.js'

const SCHNORR_PUBLIC_KEY_SIZE = 32

/**
* A Secp256k1 public key
*/
export class Secp256k1PublicKey extends PublicKey<BitcoinAddress> {
export class Secp256k1PublicKey extends PublicKey<AddressView> {
static SIZE = SCHNORR_PUBLIC_KEY_SIZE

private readonly data: Uint8Array
Expand Down Expand Up @@ -62,34 +60,12 @@ export class Secp256k1PublicKey extends PublicKey<BitcoinAddress> {
/**
* Return the Bitcoin address associated with this Secp256k1 public key
*/
override toAddress(): BitcoinAddress {
/// default version = 1 & network = testnet
return this.buildAddress(1, BitcoinNetowkType.Testnet)
override toAddress(): AddressView {
return new AddressView(this.data)
}

buildAddress(version: number, network: BitcoinNetowkType) {
const tapTweak = (a: Bytes, b: Bytes) => {
const u = schnorr.utils
const t = u.taggedHash('TapTweak', a, b)
const tn = u.bytesToNumberBE(t)
if (tn >= secp256k1.CURVE.n) throw new Error('tweak higher than curve order')
return tn
}

// Each hex char represents half a byte, hence hex address doubles the length
const u = schnorr.utils
const t = tapTweak(this.data, EmptyBytes) // t = int_from_bytes(tagged_hash("TapTweak", pubkey + h))
const P = u.lift_x(u.bytesToNumberBE(this.data)) // P = lift_x(int_from_bytes(pubkey))
const Q = P.add(secp256k1.ProjectivePoint.fromPrivateKey(t)) // Q = point_add(P, point_mul(G, t))
const tweakedPubkey = u.pointToBytes(Q)

return new BitcoinAddress(
bech32m.encode(
new BitcoinNetwork(network).bech32HRP(),
[version].concat(bech32m.toWords(tweakedPubkey)),
false,
),
)
toAddressWith(network: BitcoinNetowkType) {
return new AddressView(this.data, network)
}

/**
Expand Down
4 changes: 2 additions & 2 deletions sdk/typescript/rooch-sdk/test-e2e/case/bitcoin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe('Bitcoin Assets API', () => {
it('query utxo should be success', async () => {
const addr = testBox.keypair
.getSchnorrPublicKey()
.buildAddress(1, BitcoinNetowkType.Regtest)
.toStr()
.toAddress().bitcoinAddress.toStr()

const result = await testBox.bitcoinContainer?.executeRpcCommandRaw([], 'generatetoaddress', [
'50',
addr,
Expand Down
Loading

0 comments on commit 4466c7d

Please sign in to comment.