-
Notifications
You must be signed in to change notification settings - Fork 47
Minimal Bridging Stock Cordapp sample #117
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
base: bridging-stock-sample
Are you sure you want to change the base?
Changes from all commits
cac1512
d39ed50
1660cf6
0d422da
f1ff375
df08834
3a2a7fc
24ef067
ce6fbd2
e73a853
20f1a8c
c5afeb4
87238d2
e515d7d
236cc51
3550a5c
7f6676b
dec3cae
865c4c4
e8a3b2d
8edc2da
6495c56
ff11d9c
c9f3089
7b48a18
6d67922
2e4f61c
a15e293
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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." } | ||
} | ||
|
||
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 |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we discussed we can safely use the more generic |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you not have |
||
@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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's 1:1 mapping. |
||
bridgeToken( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this utility function itself call |
||
serviceHub, | ||
transactionBuilder, | ||
additionalOutput, | ||
additionalCommand, | ||
destination, | ||
mint, | ||
mintAuthority, | ||
quantity | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
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 theinstruction
against the expectedToken2022.mintTo
instruction object.