Skip to content

Commit

Permalink
Merge branch 'main' into abi-upload
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriel-indik authored Dec 11, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents b65d992 + 8c37ea8 commit e9a056b
Showing 25 changed files with 572 additions and 223 deletions.
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -4,6 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Bond example: run",
"request": "launch",
"runtimeArgs": [
"run",
"start"
],
"runtimeExecutable": "npm",
"type": "node",
"cwd": "${workspaceFolder}/example/bond"
},
{
"name": "Run Controller",
"type": "go",
1 change: 1 addition & 0 deletions core/go/pkg/testbed/testbed_jsonrpc_actions.go
Original file line number Diff line number Diff line change
@@ -418,6 +418,7 @@ func (tb *testbed) mapTransaction(ctx context.Context, tx *components.PrivateTra
}

return &TransactionResult{
ID: tx.ID,
EncodedCall: encodedCall,
PreparedTransaction: preparedTransaction,
PreparedMetadata: tx.PreparedMetadata,
2 changes: 2 additions & 0 deletions core/go/pkg/testbed/testbed_transaction.go
Original file line number Diff line number Diff line change
@@ -17,11 +17,13 @@
package testbed

import (
"github.com/google/uuid"
"github.com/kaleido-io/paladin/toolkit/pkg/pldapi"
"github.com/kaleido-io/paladin/toolkit/pkg/tktypes"
)

type TransactionResult struct {
ID uuid.UUID `json:"id"`
EncodedCall tktypes.HexBytes `json:"encodedCall"`
PreparedTransaction *pldapi.TransactionInput `json:"preparedTransaction"`
PreparedMetadata tktypes.RawJSON `json:"preparedMetadata"`
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
@@ -99,6 +100,8 @@ public record TransactionInput(

@JsonIgnoreProperties(ignoreUnknown = true)
public record TransactionResult(
@JsonProperty
String id,
@JsonProperty
JsonHex.Bytes encodedCall,
@JsonProperty
64 changes: 52 additions & 12 deletions doc-site/docs/tutorials/bond-issuance.md
Original file line number Diff line number Diff line change
@@ -141,6 +141,26 @@ The "hooks" configuration points it to the private hooks contract that was deplo

For this token, "restrictMinting" is disabled, because the hooks can enforce more flexible rules on both mint and transfer.

#### Create factory for atomic transactions

```typescript
await paladin1.sendTransaction({
type: TransactionType.PUBLIC,
abi: atomFactoryJson.abi,
bytecode: atomFactoryJson.bytecode,
function: "",
from: bondIssuerUnqualified,
data: {},
});
```

Many programming patterns in Paladin will require a contract on the shared ledger that
can prepare and execute atomic transactions. This is provided by the
[Atom and AtomFactory](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/shared/Atom.sol) contracts.

At least one instance of `AtomFactory` must be deployed to run this example. Once in place,
note that this same factory contract can be reused for atomic transactions of any composition.

### Bond issuance

#### Issue bond to custodian
@@ -166,8 +186,8 @@ await bondTracker.using(paladin2).beginDistribution(bondCustodian, {
discountPrice: 1,
minimumDenomination: 1,
});
const investorRegistry = await bondTracker.investorRegistry(bondIssuer);
await investorRegistry
const investorList = await bondTracker.investorList(bondIssuer);
await investorList
.using(paladin2)
.addInvestor(bondCustodian, { addr: investorAddress });
```
@@ -208,6 +228,7 @@ const bondSubscription = await newBondSubscription(
bondAddress_: notoBond.address,
units_: 100,
custodian_: bondCustodianAddress,
atomFactory_: atomFactoryAddress,
}
);
```
@@ -264,14 +285,28 @@ The `preparePayment` and `prepareBond` methods on the bond subscription contract
respective parties to encode their prepared transactions, in preparation for triggering an
atomic DvP (delivery vs. payment).

#### Prepare the atomic transaction for the swap

```typescript
await bondSubscription.using(paladin2).distribute(bondCustodian);
```

When both parties have prepared their individual transactions, they can be combined into a
single base ledger transaction. The `distribute()` method below is a private method on
the `BondSubscription` contract, but it triggers creation of a new `Atom` contract on the
base ledger which contains the encoded transactions prepared above.

Once an `Atom` is deployed, it can be used to execute all or none of the transactions it
contains. It can never be changed, executed partially, or executed more than once.

#### Approve delegation via the private contract

```typescript
await notoCash.using(paladin3).approveTransfer(investor, {
inputs: encodeStates(paymentTransfer.states.spent ?? []),
outputs: encodeStates(paymentTransfer.states.confirmed ?? []),
data: paymentTransfer.metadata.approvalParams.data,
delegate: investorCustodianGroup.address,
delegate: atomAddress,
});

await issuerCustodianGroup.approveTransition(
@@ -280,14 +315,15 @@ await issuerCustodianGroup.approveTransition(
txId: newTransactionId(),
transitionHash: bondTransfer2.metadata.approvalParams.transitionHash,
signatures: bondTransfer2.metadata.approvalParams.signatures,
delegate: investorCustodianGroup.address,
delegate: atomAddress,
}
);
```

In order for the private subscription contract to be able to facilitate the token exchange,
the base ledger address of the investor+custodian group must be designated as the approved
delegate for both the payment transfer and the bond transfer.
Once the `Atom` is deployed, it must be designated as the approved delegate for both
the payment transfer and the bond transfer. Because this binds a specific set of atomic
operations to a unique contract address, both parties can be assured that by approving
this address as a delegate, the only transaction that can take place is the agreed swap.

In the case of the payment, we use the `approveTransfer` method of Noto. For the bond,
which uses Pente custom logic to wrap the Noto token, we use the `approveTransition` method
@@ -296,15 +332,19 @@ of Pente.
#### Distribute the bond units by performing swap

```typescript
await bondSubscription.using(paladin2).distribute(bondCustodian, {
units_: 100,
await paladin2.sendTransaction({
type: TransactionType.PUBLIC,
abi: atomJson.abi,
function: "execute",
from: bondCustodianUnqualified,
to: atomAddress,
data: {},
});
```

Finally, the custodian uses the `distribute` method on the bond subscription contract to
trigger the exchange of the bond and payment.
Finally, the custodian executes the `Atom` to trigger the exchange of the bond and payment.

This private transaction will trigger the previously-prepared transactions for the cash
This will trigger the previously-prepared transactions for the cash
transfer and the bond transfer, and it will also trigger an external call to the public
bond tracker to decrease the advertised available supply of the bond.

Original file line number Diff line number Diff line change
@@ -20,10 +20,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.kaleido.paladin.pente.domain.PenteConfiguration.GroupTupleJSON;
import io.kaleido.paladin.pente.domain.helpers.BondSubscriptionHelper;
import io.kaleido.paladin.pente.domain.helpers.BondTrackerHelper;
import io.kaleido.paladin.pente.domain.helpers.NotoHelper;
import io.kaleido.paladin.pente.domain.helpers.PenteHelper;
import io.kaleido.paladin.pente.domain.helpers.*;
import io.kaleido.paladin.testbed.Testbed;
import io.kaleido.paladin.toolkit.*;
import org.junit.jupiter.api.Test;
@@ -144,6 +141,21 @@ void testBond() throws Exception {
"contracts/shared/BondTrackerPublic.sol/BondTrackerPublic.json",
"abi"
);
String atomFactoryBytecode = ResourceLoader.jsonResourceEntryText(
this.getClass().getClassLoader(),
"contracts/shared/Atom.sol/AtomFactory.json",
"bytecode"
);
JsonABI atomFactoryABI = JsonABI.fromJSONResourceEntry(
this.getClass().getClassLoader(),
"contracts/shared/Atom.sol/AtomFactory.json",
"abi"
);
JsonABI atomABI = JsonABI.fromJSONResourceEntry(
this.getClass().getClassLoader(),
"contracts/shared/Atom.sol/Atom.json",
"abi"
);

GroupTupleJSON issuerCustodianGroup = new GroupTupleJSON(
JsonHex.randomBytes32(), new String[]{bondIssuer, bondCustodian});
@@ -221,15 +233,23 @@ void testBond() throws Exception {
bondTracker.beginDistribution(bondCustodian, 1, 1);

// Add Alice as an allowed investor
var investorRegistry = bondTracker.investorRegistry(bondCustodian);
investorRegistry.addInvestor(bondCustodian, aliceAddress);
var investorList = bondTracker.investorList(bondCustodian);
investorList.addInvestor(bondCustodian, aliceAddress);

// Create the atom factory on the base ledger
String atomFactoryAddress = testbed.getRpcClient().request("testbed_deployBytecode",
"issuer",
atomFactoryABI,
atomFactoryBytecode,
new HashMap<String, String>());

// Alice deploys BondSubscription to the alice/custodian privacy group, to request subscription
// TODO: if Alice deploys, how can custodian trust it's the correct logic?
var bondSubscription = BondSubscriptionHelper.deploy(aliceCustodianInstance, alice, new HashMap<>() {{
put("bondAddress_", notoBond.address());
put("units_", 1000);
put("custodian_", custodianAddress);
put("atomFactory_", atomFactoryAddress);
}});

// Prepare the bond transfer (requires 2 calls to prepare, as the Noto transaction spawns a Pente transaction to wrap it)
@@ -243,32 +263,72 @@ void testBond() throws Exception {
bondTransfer.preparedTransaction().abi().getFirst(),
bondTransfer.preparedTransaction().data()
);
assertEquals("public", bondTransfer2.preparedTransaction().type());
var bondTransferMetadata = mapper.convertValue(bondTransfer2.preparedMetadata(), PenteHelper.PenteTransitionMetadata.class);

// Prepare the payment transfer
var paymentTransfer = notoCash.prepareTransfer(alice, bondCustodian, 1000);
assertEquals("public", paymentTransfer.preparedTransaction().type());
var paymentMetadata = mapper.convertValue(paymentTransfer.preparedMetadata(), NotoHelper.NotoTransferMetadata.class);

// Pass the prepared transfers to the subscription contract
bondSubscription.prepareBond(bondCustodian, bondTransfer2.preparedTransaction().to(), bondTransfer2.encodedCall());
bondSubscription.prepareBond(bondCustodian, bondTransfer2.preparedTransaction().to(), bondTransferMetadata.transitionWithApproval().encodedCall());
bondSubscription.preparePayment(alice, paymentTransfer.preparedTransaction().to(), paymentMetadata.transferWithApproval().encodedCall());

// Alice receives full bond distribution
var distributeTX = bondSubscription.distribute(bondCustodian);

// Look up the deployed Atom address
HashMap<String, Object> distributeReceipt = testbed.getRpcClient().request("ptx_getTransactionReceipt", distributeTX.id());
String distributeTXHash = distributeReceipt.get("transactionHash").toString();
List<HashMap<String, Object>> events = testbed.getRpcClient().request("bidx_decodeTransactionEvents",
distributeTXHash,
atomFactoryABI,
"");
var deployEvent = events.stream().filter(ev -> ev.get("soliditySignature").toString().startsWith("event AtomDeployed")).findFirst();
assertFalse(deployEvent.isEmpty());
var deployEventData = (HashMap<String, Object>) deployEvent.get().get("data");
var atomAddress = JsonHex.addressFrom(deployEventData.get("addr").toString());

// Alice approves payment transfer
notoCash.approveTransfer(
"alice",
paymentTransfer.inputStates(),
paymentTransfer.outputStates(),
paymentMetadata.approvalParams().data(),
aliceCustodianInstance.address());

// TODO: custodian should need to approve either Noto or Pente for the bond transfer
// Currently the encoded call that is returned is a fully endorsed Pente/BondTracker onTransfer(),
// which will in turn call Noto with a fully endorsed transfer().
// Either the Pente call needs to require approval, or the Noto call needs to be transferWithApproval()
// so that it requires approval.

// Alice receives full bond distribution
bondSubscription.distribute(bondCustodian, 1000);
atomAddress.toString());

// Custodian approves bond transfer
var txID = issuerCustodianInstance.approveTransition(
bondCustodian,
JsonHex.randomBytes32(),
atomAddress,
bondTransferMetadata.approvalParams().transitionHash(),
bondTransferMetadata.approvalParams().signatures());
var receipt = TestbedHelper.pollForReceipt(testbed, txID, 3000);
assertNotNull(receipt);

// Execute the Atom
txID = TestbedHelper.sendTransaction(testbed,
new Testbed.TransactionInput(
"public",
"",
bondCustodian,
atomAddress,
new HashMap<>(),
atomABI,
"execute"
));
receipt = TestbedHelper.pollForReceipt(testbed, txID, 3000);
assertNotNull(receipt);

// All prepared transactions should now be resolved
receipt = TestbedHelper.pollForReceipt(testbed, paymentTransfer.id(), 3000);
assertNotNull(receipt);
receipt = TestbedHelper.pollForReceipt(testbed, bondTransfer2.id(), 3000);
assertNotNull(receipt);
receipt = TestbedHelper.pollForReceipt(testbed, bondTransfer.id(), 3000);
assertNotNull(receipt);

// TODO: figure out how to test negative cases (such as when Pente reverts due to a non-allowed investor)

Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@

package io.kaleido.paladin.pente.domain.helpers;

import io.kaleido.paladin.testbed.Testbed;
import io.kaleido.paladin.toolkit.JsonABI;
import io.kaleido.paladin.toolkit.JsonHex;
import io.kaleido.paladin.toolkit.ResourceLoader;
@@ -81,16 +82,13 @@ public void preparePayment(String sender, JsonHex.Address to, JsonHex.Bytes enco
);
}

public void distribute(String sender, int units) throws IOException {
public Testbed.TransactionResult distribute(String sender) throws IOException {
var method = abi.getABIEntry("function", "distribute");
pente.invoke(
return pente.invoke(
method.name(),
method.inputs(),
sender,
address,
new HashMap<>() {{
put("units_", units);
}}
);
new HashMap<>());
}
}
Original file line number Diff line number Diff line change
@@ -51,8 +51,8 @@ public JsonHex.Address address() {
return address;
}

public InvestorRegistryHelper investorRegistry(String sender) throws IOException {
var method = abi.getABIEntry("function", "investorRegistry");
public InvestorListHelper investorList(String sender) throws IOException {
var method = abi.getABIEntry("function", "investorList");
var output = pente.call(
method.name(),
method.inputs(),
@@ -63,7 +63,7 @@ public InvestorRegistryHelper investorRegistry(String sender) throws IOException
address,
new HashMap<>()
);
return new InvestorRegistryHelper(pente, JsonHex.addressFrom(output.output()));
return new InvestorListHelper(pente, JsonHex.addressFrom(output.output()));
}

public String balanceOf(String sender, String account) throws IOException {
Original file line number Diff line number Diff line change
@@ -21,11 +21,11 @@
import java.io.IOException;
import java.util.HashMap;

public class InvestorRegistryHelper {
public class InvestorListHelper {
final PenteHelper pente;
final JsonHex.Address address;

InvestorRegistryHelper(PenteHelper pente, JsonHex.Address address) {
InvestorListHelper(PenteHelper pente, JsonHex.Address address) {
this.pente = pente;
this.address = address;
}
Loading

0 comments on commit e9a056b

Please sign in to comment.