Integrating with blockchains
Introduction
Noumena Cloud supports integration with blockchain networks through a dedicated Blockchain Connector service. The Blockchain Connector allows you to create wallets, deploy contracts and call functions on contracts. This guide will help you set up the Blockchain Connector and integrate it with your Noumena Cloud application.
Getting started
To get started with the Blockchain Connector, you need to enable it in your Noumena Cloud application. Once enabled, you can configure it to connect to different blockchains and deploy compiled smart contracts from a provided archive. You will also need to implement the necessary logic in NPL to send notifications to the Blockchain Connector.
Following sequence diagram illustrates the flow of an NPL application using the Blockchain Connector to create a blockchain offering. It shows the steps involved in creating an offering on the blockchain:
- Create a wallet
- Deploy a smart contract
- Mint tokens
- Whitelist the investor wallets
- Transfer tokens to the investor wallets
Dependencies
npl-connectors-library
needs to be added as a dependency to the NPL project.
<!-- pom.xml -->
<!-- add dependency -->
<dependencies>
<!--...-->
<dependency>
<groupId>com.noumenadigital.contrib</groupId>
<artifactId>npl-connectors-library</artifactId>
<version>1.0.19</version>
</dependency>
</dependencies>
<!-- unpack library -->
<build>
<plugins>
<!--...-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-npl-contrib</id>
<goals>
<goal>copy-dependencies</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<outputDirectory>${project.build.directory}/contrib</outputDirectory>
<includeGroupIds>com.noumenadigital.contrib</includeGroupIds>
<includeArtifactIds>npl-connectors-library</includeArtifactIds>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
<execution>
<id>unzip-contrib-libs</id>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<includeArtifactIds>npl-connectors-library</includeArtifactIds>
<fileMappers>
<fileMapper implementation="org.codehaus.plexus.components.io.filemappers.RegExpFileMapper">
<pattern>npl-connectors-library-.*/connector</pattern>
<replacement>connector</replacement>
</fileMapper>
</fileMappers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Configuring the Blockchain Connector
Blockchain Connector can be enabled in your Noumena Cloud application by navigating to the services section in the Noumena Cloud portal and toggling the Blockchain service on. This will allow you to use the Blockchain Connector for interacting with blockchain directly from your NPL applications.
Once the Blockchain Connector is enabled, it can be configured to connect to different blockchains and deploy compiled smart contracts from a provided archive. At the moment, Noumena Cloud supports the following blockchains:
- EVM
- Stellar
The archive with compiled smart contracts can be provided via URL. If the access to it is protected, you can specify an authentication header that will be used when fetching the smart contracts.
- EVM
- Faucet wallet private key
- RPC URL
- Stellar
- Network
- Faucet wallet URL
- RPC URL
- Horizon API URL
- Smart contracts
- URL to the smart contract archive (ZIP file)
- The
Authorization
header
Using the Blockchain Connector in NPL
Once the Blockchain Connector is configured, you can use it in your NPL applications.
The Blockchain Connector provides a set of APIs that can be used to interact with the blockchain.
You can use the Blockchain
module in your NPL code to access the Blockchain Connector APIs.
Blockchain Connector provides a set of APIs that can be used to interact with the blockchain using the NPL's notify-resume pattern. It provides 4 main types of messages:
WalletCreateMessage
: Used to create a new wallet on the blockchain.WalletFaucetFundMessage
: Used to fund a wallet with tokens from a faucet.BlockchainDeployMessage
: Used to deploy a smart contract on the blockchain.BlockchainFunctionMessage
: Used to invoke a function on a deployed smart contract.
Creating a wallet
permission[issuer] createWallet() {
var args = WalletCreate(
storageType = WalletStorageType.Vault,
walletType = WalletType.Stellar,
faucetFund = optionalOf(0)
);
notify WalletCreateMessage(args) resume createWalletCallback;
}
@api
permission[connector] createWalletCallback(res: NotifyResult<WalletCreateResponse>) | creation_pending {
match(res) {
is NotifySuccess<WalletCreateResponse> -> match(res.result.status) {
BlockchainStatus.Success -> this.createWalletSucceeded[connector](
res.result.walletAddress.getOrFail()
)
BlockchainStatus.Failure -> this.createWalletFailed[connector](res.result.details.getOrFail())
}
// This will never be called -- the blockchain response, including errors, is passed via NotifySuccess.
is NotifyFailure -> { }
};
};
Funding a wallet
permission[issuer] fudnWallet(walletAddress: Text) {
var workerWalletFaucetFundArgs = WalletFaucetFund(
address = this.address.getOrFail(),
amount = 10,
blockchainType = BlockchainType.Stellar
);
notify WalletFaucetFundMessage(
workerWalletFaucetFundArgs
) resume fundWalletCallback;
};
@api
permission[connector] fundWalletCallback(res: NotifyResult<WalletFaucetFundResponse>) | funding_pending {
match (res) {
is NotifySuccess<WalletFaucetFundResponse> -> match (res.result.status) {
BlockchainStatus.Success -> this.fundWalletSuccess[connector](res.result.hash.getOrFail())
BlockchainStatus.Failure -> this.fundWalletFailure[connector](res.result.details.getOrFail())
}
// This will never be called -- the blockchain response, including errors, is passed via NotifySuccess.
is NotifyFailure -> {}
};
};
Deploying a smart contract
When deploying a smart contract, you need to specify the path to the compiled contract within the
provided archive. Archive itself doesn't prescribe any specific structure, but you need to know the
path to the compiled contract you want to deploy, in the example below stellar/token.wasm
.
permission[issuer] startDistribution() | subscriptionCompleted {
notify BlockchainDeployMessage(
BlockchainDeploy(
senderAddress = this.wallet.address.getOrFail(),
compiledContractPath = "stellar/token.wasm",
blockchainType = blockchainType(this.productDetails.blockchain),
args = newSmartContractDeploymentArguments(
wallet = this.wallet.address.getOrFail(),
isin = this.isin,
asset = this.offeringName,
tickerSymbol = this.tickerSymbol
)
)
) resume deploymentCallback;
become blockchainDeploymentInProgress;
};
@api
permission[issuer | connector] deploymentCallback(
res: NotifyResult<BlockchainDeployResponse>
) | blockchainDeploymentInProgress, blockchainDeploymentFailed {
match(res) {
is NotifySuccess<BlockchainDeployResponse> -> match(res.result.status) {
BlockchainStatus.Success -> this.deploymentSucceeded[connector](
res.result.contractAddress.getOrFail()
)
BlockchainStatus.Failure -> this.deploymentFailed[connector](res.result.details.getOrFail())
}
// This will never be called -- the blockchain response, including errors, is passed via NotifySuccess.
is NotifyFailure -> { }
};
};
Invoking functions on the smart contract
permission[issuer | connector] mint() | blockchainDeploymentInProgress, blockchainDeploymentFailed {
notify BlockchainFunctionMessage(BlockchainFunction(
functionName = "mint",
contractAddress = this.contractAddress.getOrFail(),
senderAddress = this.wallet.address.getOrFail(),
blockchainType = blockchainType(this.productDetails.blockchain),
args = newMintArguments(this.wallet.address.getOrFail(), this.productDetails.totalTokens)
)) resume mintingCallback;
}
@api
permission[issuer | connector] mintingCallback (
res: NotifyResult<BlockchainFunctionResponse>
) | blockchainMinting, blockchainMintingFailed {
match(res) {
is NotifySuccess<BlockchainFunctionResponse> -> match(res.result.status) {
BlockchainStatus.Success -> this.mintingSucceeded[connector](res.result.hash.getOrFail())
BlockchainStatus.Failure -> this.mintingFailed[connector](res.result.details.getOrFail())
}
// This will never be called -- the blockchain response, including errors, is passed via NotifySuccess.
is NotifyFailure -> { }
};
};
Supported types of arguments for the smart contract functions:
union ArgData {
Text,
Boolean,
Int8,
Int16,
Int32,
Int64,
Int128,
Int256,
Uint8,
Uint16,
Uint32,
Uint64,
Uint128,
Uint256,
Address,
TokenBalance /* EVM: uint256, Stellar: i128 */,
TokenDecimal /* EVM: uint8, Stellar: u32 */,
Bytes32,
List<ArgData>
};