-
Notifications
You must be signed in to change notification settings - Fork 74
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
WIP/NOMERGE External signing enhancements #209
base: master
Are you sure you want to change the base?
Changes from all commits
340ea9c
d3eec6b
c4e9fec
b0c4d13
cdc9bef
babe100
5b68519
4a7681b
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,19 @@ | ||
package com.r3.corda.lib.tokens.contracts | ||
|
||
import com.r3.corda.lib.tokens.contracts.commands.IssueTokenCommand | ||
import com.r3.corda.lib.tokens.contracts.commands.MoveTokenCommand | ||
import com.r3.corda.lib.tokens.contracts.commands.RedeemTokenCommand | ||
import com.r3.corda.lib.tokens.contracts.commands.TokenCommand | ||
import com.r3.corda.lib.tokens.contracts.states.AbstractToken | ||
import com.r3.corda.lib.tokens.contracts.states.FungibleToken | ||
import com.r3.corda.lib.tokens.contracts.types.IssuedTokenType | ||
import com.r3.corda.lib.tokens.contracts.utilities.sumTokenStatesOrZero | ||
import net.corda.core.contracts.* | ||
import net.corda.core.crypto.SecureHash | ||
import net.corda.core.crypto.toStringShort | ||
import net.corda.core.internal.uncheckedCast | ||
import net.corda.core.transactions.LedgerTransaction | ||
import net.corda.core.contracts.DescribableContract | ||
import java.security.PublicKey | ||
|
||
/** | ||
|
@@ -22,7 +29,7 @@ import java.security.PublicKey | |
* to call the super method to handle the existing commands. | ||
* 3. Add a method to handle the new command in the new sub-class contract. | ||
*/ | ||
open class FungibleTokenContract : AbstractTokenContract<FungibleToken>() { | ||
open class FungibleTokenContract : AbstractTokenContract<FungibleToken>(), DescribableContract { | ||
override val accepts: Class<FungibleToken> get() = uncheckedCast(FungibleToken::class.java) | ||
|
||
companion object { | ||
|
@@ -65,8 +72,11 @@ open class FungibleTokenContract : AbstractTokenContract<FungibleToken>() { | |
inputs: List<IndexedState<FungibleToken>>, | ||
outputs: List<IndexedState<FungibleToken>>, | ||
attachments: List<Attachment>, | ||
references: List<StateAndRef<ContractState>> | ||
references: List<StateAndRef<ContractState>>, | ||
summary: List<String> | ||
) { | ||
val ourSummary = describeTransaction(inputs.map{ it.state}, outputs.map { it.state }, moveCommands.map { it.value }, attachments.map { it.id }) | ||
require(ourSummary == summary) { "The summary generated by the contract: '${ourSummary}' does not match the summary present in the tx: '${summary}'" } | ||
// Commands are grouped by Token Type, so we just need a token reference. | ||
val issuedToken: IssuedTokenType = moveCommands.first().value.token | ||
// There must be inputs and outputs present. | ||
|
@@ -138,4 +148,34 @@ open class FungibleTokenContract : AbstractTokenContract<FungibleToken>() { | |
} | ||
} | ||
} | ||
|
||
override fun describeTransaction( | ||
inputs: List<TransactionState<ContractState>>, | ||
outputs: List<TransactionState<ContractState>>, | ||
commands: List<CommandData>, | ||
attachments: List<SecureHash>): List<String> { | ||
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. are references not needed? |
||
return when(commands.first()) { | ||
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 if this throws on empty list? add edge cases 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. why do we check only first? what if we have more command data? |
||
//verify the type jar presence and correctness | ||
// Issuances should only contain one issue command. | ||
is IssueTokenCommand -> listOf("") | ||
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. Should it be implemented too? 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. or at least some todo |
||
// Moves may contain more than one move command. | ||
is MoveTokenCommand -> constructMoveDescription(inputs, outputs) | ||
// Redeems must only contain one redeem command. | ||
is RedeemTokenCommand -> listOf("I AM A REDEPMPTION SONG)") | ||
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. 😂 |
||
else -> emptyList() | ||
} | ||
} | ||
|
||
|
||
private fun constructMoveDescription(inputs: List<TransactionState<ContractState>>, outputs: List<TransactionState<ContractState>>): List<String> { | ||
val moveFrom = inputs.first().asFungibleToken().holder.owningKey | ||
val moveTo = (outputs.map { it.asFungibleToken().holder }).firstOrNull { p -> p.owningKey != moveFrom }?.owningKey ?: throw IllegalArgumentException("") | ||
val amountToMove = outputs.filter { p -> p.asFungibleToken().holder.owningKey != moveFrom }.first().asFungibleToken().amount | ||
amountToMove.displayTokenSize | ||
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 these lines do? |
||
amountToMove.quantity | ||
return listOf("Move ${amountToMove.toDecimal()} ${amountToMove.token.tokenType.tokenIdentifier} from key: ${moveFrom.toStringShort()} to key: ${moveTo.toStringShort()}") | ||
} | ||
|
||
private fun TransactionState<ContractState>.asFungibleToken() = this.data as FungibleToken | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,14 +22,17 @@ import net.corda.core.transactions.SignedTransaction | |
* @param participantSessions sessions with the participants of move transaction | ||
* @param observerSessions optional sessions with the observer nodes, to witch the transaction will be broadcasted | ||
* @param queryCriteria additional criteria for token selection | ||
* @param haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external | ||
* service is required to obtain them, to prevent blocking other work | ||
*/ | ||
class ConfidentialMoveNonFungibleTokensFlow | ||
@JvmOverloads | ||
constructor( | ||
val partyAndToken: PartyAndToken, | ||
val participantSessions: List<FlowSession>, | ||
val observerSessions: List<FlowSession> = emptyList(), | ||
val queryCriteria: QueryCriteria? = null | ||
val queryCriteria: QueryCriteria? = null, | ||
val haltForExternalSigning: Boolean = false | ||
) : FlowLogic<SignedTransaction>() { | ||
@Suspendable | ||
override fun call(): SignedTransaction { | ||
|
@@ -38,6 +41,6 @@ constructor( | |
participantSessions.forEach { it.send(TransactionRole.PARTICIPANT) } | ||
observerSessions.forEach { it.send(TransactionRole.OBSERVER) } | ||
val confidentialOutput = subFlow(ConfidentialTokensFlow(listOf(output), participantSessions)).single() | ||
return subFlow(MoveTokensFlow(input, confidentialOutput, participantSessions, observerSessions)) | ||
return subFlow(MoveTokensFlow(listOf(input), listOf(confidentialOutput), participantSessions, observerSessions, haltForExternalSigning)) | ||
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. you don't need to change it to listOf(input) etc |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,13 +8,22 @@ import com.r3.corda.lib.tokens.workflows.utilities.requireSessionsForParticipant | |
import com.r3.corda.lib.tokens.workflows.utilities.toWellKnownParties | ||
import net.corda.core.contracts.CommandWithParties | ||
import net.corda.core.flows.FinalityFlow | ||
import net.corda.core.flows.FlowExternalAsyncOperation | ||
import net.corda.core.flows.FlowLogic | ||
import net.corda.core.flows.FlowSession | ||
import net.corda.core.identity.AbstractParty | ||
import net.corda.core.identity.Party | ||
import net.corda.core.node.ServiceHub | ||
import net.corda.core.transactions.LedgerTransaction | ||
import net.corda.core.transactions.SignedTransaction | ||
import net.corda.core.transactions.TransactionBuilder | ||
import java.security.PublicKey | ||
import java.util.concurrent.Callable | ||
import java.util.concurrent.CompletableFuture | ||
import java.util.concurrent.Executors | ||
import java.util.concurrent.TimeUnit | ||
import java.util.concurrent.TimeoutException | ||
import java.util.function.Supplier | ||
|
||
/** | ||
* This flow is a wrapper around [FinalityFlow] and properly handles broadcasting transactions to observers (those which | ||
|
@@ -31,16 +40,22 @@ import net.corda.core.transactions.TransactionBuilder | |
* transaction with observers, notice that this flow can be called either with [transactionBuilder] or | ||
* [signedTransaction] | ||
* @property allSessions a set of sessions for, at least, all the transaction participants and maybe observers | ||
* @property haltForExternalSigning optional - halt the flow thread while waiting for signatures if a call to an external | ||
* service is required to obtain them, to prevent blocking other work | ||
*/ | ||
class ObserverAwareFinalityFlow private constructor( | ||
val allSessions: List<FlowSession>, | ||
val signedTransaction: SignedTransaction? = null, | ||
val transactionBuilder: TransactionBuilder? = null | ||
val transactionBuilder: TransactionBuilder? = null, | ||
val haltForExternalSigning: Boolean = false | ||
) : FlowLogic<SignedTransaction>() { | ||
|
||
constructor(transactionBuilder: TransactionBuilder, allSessions: List<FlowSession>) | ||
: this(allSessions, null, transactionBuilder) | ||
|
||
constructor(transactionBuilder: TransactionBuilder, allSessions: List<FlowSession>, haltForExternalSigning: Boolean) | ||
: this(allSessions, null, transactionBuilder, haltForExternalSigning) | ||
|
||
constructor(signedTransaction: SignedTransaction, allSessions: List<FlowSession>) | ||
: this(allSessions, signedTransaction) | ||
|
||
|
@@ -68,10 +83,42 @@ class ObserverAwareFinalityFlow private constructor( | |
} | ||
// Sign and finalise the transaction, obtaining the signing keys required from the LedgerTransaction. | ||
val ourSigningKeys = ledgerTransaction.ourSigningKeys(serviceHub) | ||
val stx = transactionBuilder?.let { | ||
serviceHub.signInitialTransaction(it, signingPubKeys = ourSigningKeys) | ||
} ?: signedTransaction | ||
?: throw IllegalArgumentException("Didn't provide transactionBuilder nor signedTransaction to the flow.") | ||
|
||
val stx = if (haltForExternalSigning) { | ||
await(SignTransactionOperation(transactionBuilder!!, ourSigningKeys, serviceHub)) | ||
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. Remove !! because it throws null pointer exception without explaining what happened. Have meaningful error handling if the transaction builder is null |
||
} else { | ||
transactionBuilder?.let { | ||
serviceHub.signInitialTransaction(it, signingPubKeys = ourSigningKeys) | ||
} ?: signedTransaction | ||
?: throw IllegalArgumentException("Didn't provide transactionBuilder nor signedTransaction to the flow.") | ||
} | ||
|
||
return subFlow(FinalityFlow(transaction = stx, sessions = finalSessions)) | ||
} | ||
|
||
} | ||
|
||
class SignTransactionOperation(private val transactionBuilder: TransactionBuilder, private val signingKeys: List<PublicKey>, | ||
private val serviceHub: ServiceHub) : FlowExternalAsyncOperation<SignedTransaction> { | ||
|
||
override fun execute(deduplicationId: String) : CompletableFuture<SignedTransaction> { | ||
val executor = Executors.newFixedThreadPool(2) | ||
return CompletableFuture.supplyAsync( | ||
Supplier { | ||
transactionBuilder.let { | ||
val future = executor.submit(Callable { | ||
serviceHub.signInitialTransaction(it, signingPubKeys = signingKeys) | ||
}) | ||
try { | ||
future.get(30, TimeUnit.MINUTES) | ||
} finally { | ||
future.cancel(true) | ||
executor.shutdown() | ||
} | ||
} | ||
}, | ||
executor | ||
) | ||
} | ||
|
||
} |
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.
Redeem doesn't need summary? It should be signed by token holder too