Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cac1512
Bridging using tokens move to self without any change and without con…
szymonsztuka Sep 3, 2025
d39ed50
Reduce formatting diff
szymonsztuka Sep 3, 2025
1660cf6
Merge branch 'bridging-stock-sample' into bridging-stock-sample-brige…
szymonsztuka Sep 3, 2025
0d422da
Fix class without a package
szymonsztuka Sep 3, 2025
f1ff375
assert if Bridge state and token amount state are outputs of the same…
szymonsztuka Sep 22, 2025
df08834
Remove use of addMoveFungibleTokens from tokens, temporally select fi…
szymonsztuka Sep 22, 2025
3a2a7fc
Move input token selection to outer flow, code simplification
szymonsztuka Sep 22, 2025
24ef067
Additional Assertion on token balance (WIP)
szymonsztuka Sep 23, 2025
ce6fbd2
Additional Assertion on token balance in Solana network, and final ba…
szymonsztuka Sep 23, 2025
e73a853
Update to 4.14
szymonsztuka Sep 24, 2025
20f1a8c
Update to change from ENT-14126 SolanaTestValidator usable in user Co…
szymonsztuka Sep 24, 2025
c5afeb4
Remove println
szymonsztuka Sep 24, 2025
87238d2
Minor changes
mkitR3 Sep 24, 2025
e515d7d
remove some duplicated checks in the test, tidy up
szymonsztuka Sep 24, 2025
236cc51
Corda participant to Solana issuance address configuration and other …
szymonsztuka Sep 25, 2025
3550a5c
Addressing review comments
szymonsztuka Sep 26, 2025
7f6676b
Initial refactoring before changing mapping key.
szymonsztuka Sep 26, 2025
dec3cae
Use FungibleToken and LinearId for mappings (instead of stock ticket).
szymonsztuka Sep 26, 2025
865c4c4
Added second token type to test and modify query inside flow to selec…
szymonsztuka Sep 29, 2025
e8a3b2d
Test util method refactoring
szymonsztuka Sep 29, 2025
8edc2da
Tidy up
szymonsztuka Sep 29, 2025
6495c56
Added Bridging Authority to the test, WIP to get the moved token to b…
szymonsztuka Sep 29, 2025
ff11d9c
Refactor SDk to own module and cordapps
szymonsztuka Sep 30, 2025
c9f3089
Refactor SDk to own module and cordapps
szymonsztuka Sep 30, 2025
7b48a18
Refactoring and mapping participant
szymonsztuka Sep 30, 2025
6d67922
Remove import
szymonsztuka Sep 30, 2025
2e4f61c
Filtering a single previous owner of a token in nicer way
szymonsztuka Sep 30, 2025
a15e293
Remove Solana SDK/Common from Sample Cordapp dependencies - not used
szymonsztuka Sep 30, 2025
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
7 changes: 4 additions & 3 deletions Tokens/constants.properties
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
cordaReleaseGroup=com.r3.corda
cordaCoreReleaseGroup=net.corda
cordaVersion=4.13-SNAPSHOT
cordaCoreVersion=4.13-SNAPSHOT
cordaVersion=4.14-SNAPSHOT
cordaCoreVersion=4.14-SNAPSHOT
gradlePluginsVersion=5.1.1
kotlinVersion=1.9.20
junitVersion=4.12
quasarVersion=0.9.0_r3
log4jVersion =2.23.1
platformVersion=140
slf4jVersion=2.0.12
nettyVersion=4.1.77.Final
nettyVersion=4.1.77.Final
solana4jVersion=1.2.0
57 changes: 57 additions & 0 deletions Tokens/stockpaydividend/bridging-contracts/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
apply plugin: 'net.corda.plugins.cordapp'
apply plugin: 'net.corda.plugins.quasar-utils'

cordapp {
targetPlatformVersion corda_platform_version.toInteger()
minimumPlatformVersion corda_platform_version.toInteger()
workflow {
name "Bridging Contracts"
vendor "Corda Open Source"
licence "Apache License, Version 2.0"
versionId 1
}
}

sourceSets {
main {
resources {
srcDir rootProject.file("config/dev")
}
}
test {
resources {
srcDir rootProject.file("config/test")
}
}
integrationTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integrationTest/kotlin')
}
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

// Corda dependencies.
cordaProvided "$corda_core_release_group:corda-core:$corda_core_release_version"
cordaProvided "$corda_release_group:corda:$corda_release_version"

// Token SDK dependencies.
cordaProvided "$tokens_release_group:tokens-contracts:$tokens_release_version"
cordaProvided "$tokens_release_group:tokens-workflows:$tokens_release_version"

cordaProvided "$corda_release_group:corda-solana-sdk:$corda_release_version"
cordaProvided "$corda_release_group:corda-solana-common:$corda_release_version"
}

task integrationTest(type: Test, dependsOn: []) {
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}

test {
jvmArgs = rootProject.ext.testJvmArgs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.r3.corda.lib.tokens.bridging.contracts

import com.lmax.solana4j.programs.TokenProgramBase
import com.r3.corda.lib.tokens.bridging.states.BridgedAssetLockState
import com.r3.corda.lib.tokens.contracts.commands.MoveTokenCommand
import com.r3.corda.lib.tokens.contracts.states.FungibleToken
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.Contract
import net.corda.core.identity.Party
import net.corda.core.transactions.LedgerTransaction
import net.corda.solana.sdk.instruction.Pubkey
import net.corda.solana.sdk.instruction.SolanaInstruction
import net.corda.solana.sdk.internal.Token2022
import java.nio.ByteBuffer
import java.nio.ByteOrder

class BridgingContract : Contract {
override fun verify(tx: LedgerTransaction) {
val bridgingCommands = tx.commandsOfType<BridgingCommand>()

require(bridgingCommands.size == 1) { "Bridging transactions must have single bridging command" }

when (val bridgingCommand = bridgingCommands.single().value) {
is BridgingCommand.BridgeToSolana -> verifyBridging(tx, bridgingCommand)
}
}

private fun verifyBridging(tx: LedgerTransaction, bridgingCommand: BridgingCommand.BridgeToSolana) {
val lockState = tx.outputsOfType<BridgedAssetLockState>().singleOrNull()

require(lockState != null) { "Bridging transaction must have exactly one BridgedAssetLockstate as output" }

require(lockState.participants.single() == bridgingCommand.bridgeAuthority) { "BridgedAssetLockstate must be owned by bridging authority" }

val moveCommands = tx.commandsOfType<MoveTokenCommand>()

require(moveCommands.size == 1) { "Bridging must have one move command to lock token" }

val lockedSum = tx.outputsOfType<FungibleToken>()
.filter { it.holder == bridgingCommand.bridgeAuthority } // TODO this is mute point for now, change to != bridgeAuthority, to filter only states owned by CI ...
// ... currently can't distinguish between locked and a change, both are for same holder
.sumOf {
it.amount.toDecimal().toLong()
}

val instruction = tx.notaryInstructions.singleOrNull() as? SolanaInstruction

require(instruction != null) { "Exactly one Solana mint instruction required" }

require(instruction.programId == Token2022.PROGRAM_ID) { "Solana program id must be Token2022 program" }

require(instruction.accounts[1].pubkey == bridgingCommand.targetAddress) { "Target in instructions does not match command" }

@Suppress("MagicNumber")
require(instruction.data.size == 9) { "Expecting 9 bytes of instruction data" }

val instructionBytes = ByteBuffer.wrap(instruction.data.bytes).order(ByteOrder.LITTLE_ENDIAN)

val tokenInstruction = instructionBytes.get().toInt()

val amount = instructionBytes.getLong()
require(tokenInstruction == TokenProgramBase.MINT_TO_INSTRUCTION) { "Token instruction must be MINT_TO_INSTRUCTION" }

require(amount == lockedSum) { "Locked amount of $lockedSum must match requested mint amount $amount." }
Comment on lines +50 to +64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is brittle and hard to read. SolanaInstruction implements equals/hashCode and so you can instead compare the instruction against the expected Token2022.mintTo instruction object.

}

sealed interface BridgingCommand : CommandData {

data class BridgeToSolana(val targetAddress: Pubkey, val bridgeAuthority: Party) : BridgingCommand
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.r3.corda.lib.tokens.bridging.states

import com.r3.corda.lib.tokens.bridging.contracts.BridgingContract
import net.corda.core.contracts.BelongsToContract
import net.corda.core.contracts.ContractState
import net.corda.core.identity.AbstractParty

@BelongsToContract(BridgingContract::class)
class BridgedAssetLockState(override val participants: List<AbstractParty>) : ContractState
68 changes: 68 additions & 0 deletions Tokens/stockpaydividend/bridging-flows/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
apply plugin: 'net.corda.plugins.cordapp'
apply plugin: 'net.corda.plugins.quasar-utils'

cordapp {
targetPlatformVersion corda_platform_version.toInteger()
minimumPlatformVersion corda_platform_version.toInteger()
workflow {
name "Token Bridging Flows"
vendor "Corda Open Source"
licence "Apache License, Version 2.0"
versionId 1
}
}

sourceSets {
main {
resources {
srcDir rootProject.file("config/dev")
}
}
test {
resources {
srcDir rootProject.file("config/test")
}
}
integrationTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integrationTest/kotlin')
}
}
}


dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation "junit:junit:$junit_version"

// Corda dependencies.
cordaProvided "$corda_core_release_group:corda-core:$corda_core_release_version"
cordaProvided "$corda_release_group:corda:$corda_release_version"
testImplementation "$corda_release_group:corda-node-driver:$corda_release_version"
testImplementation "$corda_core_release_group:corda-core-test-utils:$corda_core_release_version"

testImplementation "$corda_release_group:corda-solana-common:$corda_release_version"
testImplementation "$corda_release_group:corda-test-utils:$corda_release_version"

// CorDapp dependencies.
cordapp project(":bridging-contracts")

// Token SDK dependencies.
cordaProvided "$tokens_release_group:tokens-contracts:$tokens_release_version"
cordaProvided "$tokens_release_group:tokens-workflows:$tokens_release_version"

cordaProvided "$corda_release_group:corda-solana-sdk:$corda_release_version"
cordaProvided "$corda_release_group:corda-solana-common:$corda_release_version"
}

task integrationTest(type: Test, dependsOn: []) {
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}

test {
jvmArgs = rootProject.ext.testJvmArgs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.r3.corda.lib.tokens.bridging.flows

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.tokens.bridging.contracts.BridgingContract
import com.r3.corda.lib.tokens.bridging.states.BridgedAssetLockState
import com.r3.corda.lib.tokens.contracts.states.FungibleToken
import com.r3.corda.lib.tokens.contracts.types.TokenPointer
import com.r3.corda.lib.tokens.workflows.flows.move.AbstractMoveTokensFlow
import com.r3.corda.lib.tokens.workflows.flows.move.MoveTokensFlowHandler
import com.r3.corda.lib.tokens.workflows.flows.move.addMoveTokens
import com.r3.corda.lib.tokens.workflows.utilities.sessionsForParties
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByService
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.solana.sdk.instruction.Pubkey

/**
* Initiating flow used to bridge token of the same party.
*
* @param observers optional observing parties to which the transaction will be broadcast
*/
@StartableByService
@InitiatingFlow
class BridgeFungibleTokenFlow(
val holder: AbstractParty,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed. We can figure out the holder from the token.

val observers: List<Party> = emptyList(),
val token: StateAndRef<FungibleToken>, //TODO should be FungibleToken, TODO change to any TokenType would need amendments to UUID retrieval below

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these TODOs are no longer needed?

val bridgeAuthority: Party
) : FlowLogic<SignedTransaction>() {

@Suspendable
override fun call(): SignedTransaction {
val participants = listOf(holder)
val observerSessions = sessionsForParties(observers)
val participantSessions = sessionsForParties(participants)

val additionalOutput: ContractState = BridgedAssetLockState(listOf(ourIdentity))

val cordaTokenId = (token.state.data.amount.token.tokenType as TokenPointer<*>).pointer.pointer.id

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed we can safely use the more generic token.state.data.amount.token.tokenType.tokenIdentifier bc TokenPointer puts its pointer ID in the tokenIdentifier field.


val owners = previousOwnersOf(token).map { serviceHub.identityService.wellKnownPartyFromAnonymous(it) ?: it }
val singlePreviousOwner = owners.singleOrNull { it is Party } as Party?
require(singlePreviousOwner != null) {
"Cannot find previous owner of the token to bridge, or multiple found: $owners"
}
val solanaAccountMapping = serviceHub.cordaService(SolanaAccountsMappingService::class.java)
val destination =
solanaAccountMapping.participants[singlePreviousOwner.name]!! //TODO handle null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw an exception if previous owner is not configured.

val mint = solanaAccountMapping.mints[cordaTokenId]!! //TODO handle null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

val mintAuthority = solanaAccountMapping.mintAuthorities[cordaTokenId]!! //TODO handle null
val additionalCommand = BridgingContract.BridgingCommand.BridgeToSolana(
destination,
bridgeAuthority
)

return subFlow(
InternalBridgeFungibleTokenFlow(
participantSessions = participantSessions,
observerSessions = observerSessions,
token = token,
additionalOutput = additionalOutput,
additionalCommand = additionalCommand,
destination = destination,
mint = mint,
mintAuthority = mintAuthority
)
)
}

fun previousOwnersOf(output: StateAndRef<FungibleToken>): Set<AbstractParty> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should only be returning a single party, and the filter should be based on a the extact same token with just the owner being different.

However, as we've been discussing, we may not need to do this with the new design, so ignore this comment for now.

val txHash = output.ref.txhash
val stx = serviceHub.validatedTransactions.getTransaction(txHash)
?: error("Producing transaction $txHash not found")

val inputTokens: List<FungibleToken> =
stx.toLedgerTransaction(serviceHub).inputsOfType<FungibleToken>()

return inputTokens.map { it.holder }.toSet()
}
}

/**
* Responder flow for [BridgeFungibleTokenFlow].
*/
@InitiatedBy(BridgeFungibleTokenFlow::class)
class BridgeFungibleTokensHandler(val otherSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() = subFlow(MoveTokensFlowHandler(otherSession))
}

class InternalBridgeFungibleTokenFlow

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you not have BridgeFungibleTokenFlow directly extend AbstractMoveTokensFlow instead of this intermediate internal flow?

@JvmOverloads
constructor(
override val participantSessions: List<FlowSession>,
override val observerSessions: List<FlowSession> = emptyList(),
val token: StateAndRef<FungibleToken>,
val additionalOutput: ContractState,
val additionalCommand: BridgingContract.BridgingCommand,
val destination: Pubkey,
val mint: Pubkey,
val mintAuthority: Pubkey
) : AbstractMoveTokensFlow() { //TODO move away from this abstract class, it's progress tracker mention only token move

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the issue with the progress tracker? Looking at the SDK, I think we should use this class. It seems to be the main flow to use for moving tokens and so any customer using the SDK will also be using it.


@Suspendable
override fun addMove(transactionBuilder: TransactionBuilder) {

val amount = token.state.data.amount
val holder = ourIdentity //TODO confidential identity
val output = FungibleToken(amount, holder)
addMoveTokens(transactionBuilder = transactionBuilder, inputs = listOf(token), outputs = listOf(output))

val quantity = amount.toDecimal()
.toLong() //TODO this is quantity for Solana, should it be 1 to 1 what is bridged on Corda?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's 1:1 mapping.

bridgeToken(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this utility function itself call addMoveTokens rather than assume it has been called? It makes a better utility function that correctly sets up the tx builder. Otherwise, bridgeToken is assuming the output and input states are already correctly present.

serviceHub,
transactionBuilder,
additionalOutput,
additionalCommand,
destination,
mint,
mintAuthority,
quantity
)
}
}
Loading