diff --git a/ARCs/arc-0063.md b/ARCs/arc-0063.md new file mode 100644 index 000000000..2ded4456e --- /dev/null +++ b/ARCs/arc-0063.md @@ -0,0 +1,320 @@ +--- +arc: 63 +title: Lsig Plug-In Signer for Msig Vault +description: Delegated multisig-account controlled by one account +author: Stéphane BARROSO (@SudoWeezy) +discussions-to: https://github.com/algorandfoundation/ARCs/issues/303 +status: Draft +type: Standards Track +category: ARC +created: 2024-07-16 +--- + +## Abstract + +This ARC proposes a method for creating a delegated multisig account controlled by one account and a Logic Signature (Lsig). + +## Motivation + +The motivation behind this ARC is to extend Algorand account features by enabling third-party "Plug-Ins" using a combination of delegated Lsig and Multi-Signature accounts, which act as vaults. This approach allows anyone to sign the Lsig for the vault, while maintaining security and control through a classic algorand account. + +## Specification + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in RFC-2119 + +### Components + +1. **Owner Account**: Vault's Owner +1. **Lsig Plug-In**: Provided by a third party. +1. **Plug-In Signer**: Created by generating a new key pair +1. **1/2 Msig Account**: Comprises the owner's address, and the plug-in signer. + +### Implementation 1 + +We will use the following Logic plug-in for illustrative purposes: + +**DO NOT USE IN PRODUCTION** + +```python +teal_program = """ +#pragma version 10 +txn TypeEnum +pushint 4 +== +txn AssetAmount +pushint 0 +== +&& +txn RekeyTo +global ZeroAddress +== +&& +txn Fee +global MinTxnFee +== +&& +return +""" +compiled_program = client.compile(teal_program) +program = base64.b64decode(compiled_program["result"]) +lsig = transaction.LogicSigAccount(program) +``` + +> This give opt-in control over the signer + +#### 1. **Generate Plug-In Signer** + +- Generate a random new account that we will only use once. + +```python +plug_in_sk, plug_in_addr = account.generate_account() +``` + +#### 2. **Sign Lsig with Plug-In Signer** + +- Sign the Lsig using the plug-in signer. +- Publish the public signature on the blockchain. + +```python + public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair(base64.b64decode(plug_in_sk)[: constants.key_len_bytes]) + message = constants.logic_prefix + program + raw_signed = nacl.bindings.crypto_sign(message, secret_key) + crypto_sign_BYTES = nacl.bindings.crypto_sign_BYTES + signature = nacl.encoding.RawEncoder.encode(raw_signed[:crypto_sign_BYTES]) + plug_in_public_sig = base64.b64encode(signature).decode() +``` + +> You can achieve the same result like this: + +```python +lsig = transaction.LogicSigAccount(program).sign(plug_in_sk) +plug_in_public_sig = lsig.lsig.sig +``` + +#### 3. **Create 1/2 Msig Account** + +- Create a multi-signature account with owner address, and the plug-in signer. + +```python +owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr]) +``` + +- Add a transaction note to the transaction to help third party to retrieve signer and vault information. + +```json + { + "pk": plug_in_addr, + "sk": plug_in_public_sig, + "lsig": lsig.address(), + } +``` + +- Prefix the note following the [ARC-2](./arc-0002.md) standard. `arc63:j` + +```python +ptxn = transaction.PaymentTxn( + owner_addr, sp, owner_vault_msig.address(), int(1e6), note=f"arc_63:j{note_field}" +).sign(owner_sk) +``` + +#### 4. **Opt-In to Msig Vault** + +- Anyone can opt-in to the Msig vault using the plug-in signer’s public address and the published signature. + +```python +optin_txn = AssetTransferTxn( + sender=owner_vault_msig.address(), + sp=sp, + receiver=owner_vault_msig.address(), + amt=0, + index=a_id, +) +lsig.lsig.msig = owner_vault_msig +lsig.append_to_multisig(plug_in_sk) # signature from plug_in_public +lstx = LogicSigTransaction(optin_txn, lsig) +``` + +### Implementation 2 + +We will use the following Lsig plug-in for our illustrative purposes: + +**DO NOT USE IN PRODUCTION** + +```python +teal_program = f""" +#pragma version 10 +txn TypeEnum +int appl +== +txn ApplicationID +int {app_client.app_id} +== +&& +txn RekeyTo +global ZeroAddress +== +&& +txn Fee +global MinTxnFee +== +&& +return +""" +compiled_program = client.compile(teal_program) +program = base64.b64decode(compiled_program["result"]) +lsig = transaction.LogicSigAccount(program) +``` + +> This allow the application with the id `app_client.app_id` to control the signer + +To be consistant with the previous example, we will use a similar opt-in process. + +```python +class SmartApp(ARC4Contract): + def __init__(self) -> None: + self.db = BoxMap(Account, String, key_prefix="") + + @abimethod() + def opt_in(self, id: UInt64, account: Account) -> None: + itxn.AssetTransfer( + asset_amount=0, + xfer_asset=id, + sender=account, + asset_receiver=account, + fee=1000, + ).submit() + + @abimethod() + def set_public_sig(self, account: Account, sig: String) -> bool: + self.db[account] = sig + return self.db[account] == sig + + @abimethod(readonly=True) + def get_public_sig(self, account: Account) -> String: + return self.db[account] +``` + +### 1. [Generate Plug-In Signer](./arc-0063.md#1-generate-plug-in-signer) + +### 2. [Sign Lsig with Plug-In Signer](./arc-0063.md#2-sign-lsig-with-plug-in-signer) + +#### 3. **Create 1/2 Msig Account** and execute an app Call + +- Create a multi-signature account with owner address, and the plug-in signer. + +```python +owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr]) +``` + +- Publish the public signature by using set_public_sig to the app. + +```python +response = app_client.set_public_sig( + account=owner_addr, + sig=plug_in_public_sig, + transaction_parameters=algokit_utils.TransactionParameters( + boxes=[(app_client.app_id, encoding.decode_address(owner_addr))], + accounts=[owner_addr] + ) +) +``` + +- Get the transaction set_public_sig from the app. + +```python +response = app_client.get_public_sig( + account=owner_addr, + transaction_parameters=algokit_utils.TransactionParameters( + boxes=[(app_client.app_id, encoding.decode_address(owner_addr))] + ), +) +``` + +#### 4. **App in control of Msig Vault** + +- Anyone can now call the app to opt-in any asset to the Msig vault using the plug-in signer’s public address and the published signature. + +```python +composer.opt_in( + id=a_id, + account=app_client.app_address, + transaction_parameters=algokit_utils.TransactionParameters( + foreign_assets=[a_id], signer=app_client.signer, + sender=owner_vault_msig.address() + ), +) +opt_in_txn = composer.atc.txn_list[0].txn + +lsig.lsig.msig = owner_vault_msig +lsig.lsig.msig.subsigs[1].signature = base64.b64decode(plug_in_public_sig) +lstx = transaction.LogicSigTransaction(opt_in_txn, lsig) +``` + +### Diagram + +```mermaid +graph TD + subgraph signer + PKS[PK] + SKS[SK] + PKS ~~~ SKS + end + subgraph msigVault + SKVS[Public Signature] + SKVA[Public Address] + PKV[ownerPK] + PKV ~~~ SKVA + PKV ~~~ SKVS + end + subgraph owner + PKO[PK] + end + + ta[[Throwaway Account]] + + lsig((Lsig Plug In)) + + sign((Sign)) + + ta --> signer + PKO --> PKV + SKS --> sign + lsig --- sign + sign --> SKVS + PKS --> SKVA + msigVault ~~~ lsig +``` + +## Rationale + +The rationale for this design is to leverage third-party Lsig plug-ins. By note storing the plug-in signer private key, we mitigate risks associated with its misuse, while the multi-signature account setup ensures controlled access and flexibility in asset management. + +## Backwards Compatibility + +This ARC introduces no backward incompatibilities. It builds upon existing Algorand functionalities, ensuring seamless integration with current systems. + +## Reference Implementation + +An example implementation in Python is provided, demonstrating the creation of a plug-in signer, signing an Lsig, and opting into a multi-signature vault. + +### Only with Lsig + +[Create_Opt_in_Plug_in](../assets/arc-0063/create_plugin.py) + +### Delegating the Lsig to an App + +[Deploy config](../assets/arc-0063/deploy_config.py) +[Contract](../assets/arc-0063/contract.py) + +> This need to be run with Algokit + +## Security Considerations + +Even if the plug-in signer is rekeyed, the private key can still sign new lsigs, which is why the private key should not be accessible by anyone after the signature. +This step is crucial to prevent any unauthorized use of the signer post-creation. + +If the Multi-signature vault accounts is rekeyd to any other account (Msig or traditional), it will keep the same public address and will not be delegated anymore. + +## Copyright + +Copyright and related rights waived via CCO. diff --git a/assets/arc-0063/contract.py b/assets/arc-0063/contract.py new file mode 100644 index 000000000..6e9c9d67e --- /dev/null +++ b/assets/arc-0063/contract.py @@ -0,0 +1,26 @@ +from algopy import ARC4Contract, Account, UInt64, itxn, BoxMap +from algopy.arc4 import abimethod, String + + +class SmartApp(ARC4Contract): + def __init__(self) -> None: + self.db = BoxMap(Account, String, key_prefix="") + + @abimethod() + def opt_in(self, id: UInt64, account: Account) -> None: + itxn.AssetTransfer( + asset_amount=0, + xfer_asset=id, + sender=account, + asset_receiver=account, + fee=1000, + ).submit() + + @abimethod() + def set_public_sig(self, account: Account, sig: String) -> bool: + self.db[account] = sig + return self.db[account] == sig + + @abimethod(readonly=True) + def get_public_sig(self, account: Account) -> String: + return self.db[account] diff --git a/assets/arc-0063/create_plugin.py b/assets/arc-0063/create_plugin.py new file mode 100644 index 000000000..b47a96648 --- /dev/null +++ b/assets/arc-0063/create_plugin.py @@ -0,0 +1,181 @@ +from algosdk import mnemonic, transaction, encoding, constants, v2client, account +from typing import Dict, Any +import base64 +import nacl +import json + +algod_address = "http://localhost:4001" # Adjust if using a different port +algod_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +client = v2client.algod.AlgodClient(algod_token, algod_address) +indexer_address = "http://localhost:8980" # Adjust if using a different port +indexer_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +indexer = v2client.indexer.IndexerClient(indexer_token, indexer_address) +sp = client.suggested_params() + +teal_program = """ +#pragma version 10 +txn TypeEnum +pushint 4 +== +txn AssetAmount +pushint 0 +== +&& +txn AssetCloseTo +global ZeroAddress +== +&& +txn RekeyTo +global ZeroAddress +== +&& +txn Fee +global MinTxnFee +== +&& +return +""" + +compiled_program = client.compile(teal_program) +program = base64.b64decode(compiled_program["result"]) +lsig = transaction.LogicSigAccount(program) + +add = [ + { + "address": "45UO5ZGAAV3VSUFWPY72UITVNSWKLSYJBBALU2O56E32QQYHXHCI5D2PDA", + "mnemonic": "dumb pencil plastic isolate butter ribbon glide tragic pulse empty grape double glass stadium disorder riot agent donkey city weird shadow bubble ladder absent kidney", + }, + { + "address": "6QZBRTHUT4P4D26HBW7NSJJ26P3WV4NWXLBF7AB5TVDVJFXLFLN6RMZQKI", + "mnemonic": "sense gate people glare window bright betray tiny group subject blast gasp cargo safe play news inhale evolve luggage coil biology wide custom absorb trust", + } +] + +owner_sk, owner_addr = mnemonic.to_private_key(add[0]["mnemonic"]), add[0]["address"] +asa_creator_sk, asa_creator_addr = mnemonic.to_private_key(add[1]["mnemonic"]), add[1]["address"] + + #******************** Plug_IN_Signer Account Generation ****************************# +if True: + plug_in_sk, plug_in_addr = account.generate_account() +else: + plug_in_sk = "Cq9JfCTzMp9bSKVNxuF2YsNm0fS9RsshOVbN6I8Av5zbhEH2I1Qd8UN6UhgOfD1REDY9/pNjPy+D++ib2xTAAg==" + plug_in_addr = "3OCED5RDKQO7CQ32KIMA47B5KEIDMPP6SNRT6L4D7PUJXWYUYABBZG56JE" + #******************** Plug_IN_Signer Public Signature ****************************# +public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair(base64.b64decode(plug_in_sk)[: constants.key_len_bytes]) +message = constants.logic_prefix + program +raw_signed = nacl.bindings.crypto_sign(message, secret_key) +crypto_sign_BYTES = nacl.bindings.crypto_sign_BYTES +signature = nacl.encoding.RawEncoder.encode(raw_signed[:crypto_sign_BYTES]) +plug_in_public_sig = base64.b64encode(signature).decode() + +owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr]) + + +rekey_info = indexer.search_transactions_by_address(plug_in_addr, rekey_to=True)["transactions"] + +if (int(client.account_info(owner_vault_msig.address())["amount"]) == 0): + #********************* MSIG VAULT Generation **************************************# + #********************* MSIG VAUL Funding **************************************# + note_field = json.dumps({ + "pk": plug_in_addr, + "sk": plug_in_public_sig, + "lsig": lsig.address() + }) + ptxn_vault = transaction.PaymentTxn( + owner_addr, sp, owner_vault_msig.address(), int(1e6), note=f"arc63:j{note_field}" + ) + + #******************** Fund Signer before rekey ADDRESS ****************************# + + ptxn_signer = transaction.PaymentTxn( + owner_addr, sp, plug_in_addr, int(1e5 + 1e3) + ) + #******************** REKEY PLUG_IN TO 0 ADDRESS ****************************# + + zero_msig = transaction.Multisig(1,1,[constants.ZERO_ADDRESS]) + rekey_txn = transaction.PaymentTxn( + plug_in_addr, sp, plug_in_addr, 0, rekey_to=zero_msig.address() + ) + transaction.assign_group_id([ptxn_vault, ptxn_signer, rekey_txn]) + + signed_ptxn_vault = ptxn_vault.sign(owner_sk) + signed_ptxn_signer = ptxn_signer.sign(owner_sk) + + signed_rekey = rekey_txn.sign(plug_in_sk) + + signed_group = [signed_ptxn_vault, signed_ptxn_signer, signed_rekey] + print(signed_group) + txid = client.send_transactions(signed_group) + result: Dict[str, Any] = transaction.wait_for_confirmation( + client, txid, 4 + ) + print(f"txID: {txid} confirmed in round: {result.get('confirmed-round', 0)}") + +print("owner vault addresses : ", owner_vault_msig.address()) #NUVYGSZCMMH65PGYGPRB63JNZMMIKT6ROK5HNM3BKVKLSNL77FTB7DKMJU +for i in owner_vault_msig.subsigs: + print("owner vault address: ", encoding.encode_address(i.public_key), base64.b64encode(i.public_key)) + + +asa_creator_info = client.account_info(asa_creator_addr) +if 'assets' in asa_creator_info and (len(asa_creator_info['assets']) > 0): + a_id = client.account_info(asa_creator_addr)['assets'][0]['asset-id'] +else: + #******************** asa_creator CREATE ASA ****************************# + print("asa_creator Create ASA") + actxn = transaction.AssetConfigTxn( sender=asa_creator_addr, sp=sp, default_frozen=False, unit_name="rug2", asset_name="2 Really Useful Gift", manager=asa_creator_addr, reserve=asa_creator_addr, freeze=asa_creator_addr, clawback=asa_creator_addr, url="https://path/to/my/asset/details", total=10, decimals=0, ) + sactxn = actxn.sign(asa_creator_sk) + tx_id = client.send_transaction(sactxn) + print(f"Sent asset create transaction with txid: {tx_id}") + # Wait for the transaction to be confirmed + results = transaction.wait_for_confirmation(client, tx_id, 4) + a_id = results['asset-index'] + print(f"Result confirmed in round: {results['confirmed-round']} ASA ID : {results['asset-index']}") +print(f'asa_creator created 10 ASA: {a_id}') + + + #******************** MSIG VAULT OPT IN ****************************# +optin_txn = transaction.AssetTransferTxn( + sender=owner_vault_msig.address(), + sp=sp, + receiver=owner_vault_msig.address(), + amt=0, + index=a_id, +) +lsig.lsig.msig = owner_vault_msig +lsig.append_to_multisig(plug_in_sk) + +assert (lsig.lsig.msig.subsigs[1].signature == base64.b64decode(plug_in_public_sig)) # signature from plug_in_public +lstx = transaction.LogicSigTransaction(optin_txn, lsig) + + + +optin_txid = client.send_transaction(lstx) + + +print(f"Sent Msig Vault Opt-in with txid: {optin_txid}") +# Wait for the transaction to be confirmed +results = transaction.wait_for_confirmation(client, optin_txid, 4) +print(f"Result confirmed in round: {results['confirmed-round']}") +print(" #********************Opt Out ASA***************************#") +owner_vault_msig = transaction.Multisig( + 1, + 1, + [owner_addr, plug_in_addr] +) + +optout_txn = transaction.AssetCloseOutTxn( + sender=owner_vault_msig.address(), + sp=sp, + receiver=owner_vault_msig.address(), + index=a_id +) + +msig_txn = transaction.MultisigTransaction(optout_txn, owner_vault_msig) +msig_txn.sign(owner_sk) +optout_txn = client.send_transaction(msig_txn) + +print(f"Sent Msig Vault Opt-out with txid: {optout_txn}") +# Wait for the transaction to be confirmed +results = transaction.wait_for_confirmation(client, optout_txn, 4) +print(f"Result confirmed in round: {results['confirmed-round']}") +print(f"{owner_vault_msig.address()} Opted - Out {a_id}") diff --git a/assets/arc-0063/deploy_config.py b/assets/arc-0063/deploy_config.py new file mode 100644 index 000000000..a289c0018 --- /dev/null +++ b/assets/arc-0063/deploy_config.py @@ -0,0 +1,269 @@ +import logging +import algokit_utils +from algosdk import ( + mnemonic, + transaction, + constants, + account, + encoding +) +from algosdk.v2client.algod import AlgodClient +from algosdk.v2client.indexer import IndexerClient +from typing import Dict, Any +import base64 +import nacl + +logger = logging.getLogger(__name__) + + +add = [ + { + "address": "45UO5ZGAAV3VSUFWPY72UITVNSWKLSYJBBALU2O56E32QQYHXHCI5D2PDA", + "mnemonic": "dumb pencil plastic isolate butter ribbon glide tragic pulse empty grape double glass stadium disorder riot agent donkey city weird shadow bubble ladder absent kidney", + }, + { + "address": "6QZBRTHUT4P4D26HBW7NSJJ26P3WV4NWXLBF7AB5TVDVJFXLFLN6RMZQKI", + "mnemonic": "sense gate people glare window bright betray tiny group subject blast gasp cargo safe play news inhale evolve luggage coil biology wide custom absorb trust", + }, +] + +owner_sk, owner_addr = mnemonic.to_private_key(add[0]["mnemonic"]), add[0]["address"] +asa_creator_sk, asa_creator_addr = ( + mnemonic.to_private_key(add[1]["mnemonic"]), + add[1]["address"], +) + + +# define deployment behaviour based on supplied app spec +def deploy( + algod_client: AlgodClient, + indexer_client: IndexerClient, + app_spec: algokit_utils.ApplicationSpecification, + deployer: algokit_utils.Account, +) -> None: + from smart_contracts.artifacts.smart_app.smart_app_client import SmartAppClient, Composer, AtomicTransactionComposer + + app_client = SmartAppClient( + algod_client, + creator=deployer, + indexer_client=indexer_client, + ) + dispenser = algokit_utils.get_dispenser_account(algod_client) + + sp = algod_client.suggested_params() + if (int(algod_client.account_info(owner_addr)["amount"]) <= 1e7): + ptxn_signer = transaction.PaymentTxn( + dispenser.address, sp, owner_addr, int(1e7) + ).sign(dispenser.private_key) + algod_client.send_transaction(ptxn_signer) + + asa_creator_info = algod_client.account_info(asa_creator_addr) + if "assets" in asa_creator_info and (len(asa_creator_info["assets"]) > 0): + a_id = algod_client.account_info(asa_creator_addr)["assets"][0]["asset-id"] + else: + # ******************** asa_creator CREATE ASA ****************************# + ptxn_signer = transaction.PaymentTxn( + dispenser.address, sp, asa_creator_addr, int(2e5 + 1e3) + ).sign(dispenser.private_key) + algod_client.send_transaction(ptxn_signer) + logger.info("asa_creator Create ASA") + actxn = transaction.AssetConfigTxn( + sender=asa_creator_addr, + sp=sp, + default_frozen=False, + unit_name="rug2", + asset_name="2 Really Useful Gift", + manager=asa_creator_addr, + reserve=asa_creator_addr, + freeze=asa_creator_addr, + clawback=asa_creator_addr, + url="https://path/to/my/asset/details", + total=1, + decimals=0, + ) + sactxn = actxn.sign(asa_creator_sk) + tx_id = algod_client.send_transaction(sactxn) + logger.info(f"Sent asset create transaction with txid: {tx_id}") + # Wait for the transaction to be confirmed + results = transaction.wait_for_confirmation(algod_client, tx_id, 4) + a_id = results["asset-index"] + logger.info( + f"Result confirmed in round: { + results['confirmed-round']} ASA ID : {results['asset-index']}" + ) + logger.info(f"asa_creator created 10 ASA: {a_id}") + + app_client.deploy( + on_schema_break=algokit_utils.OnSchemaBreak.AppendApp, + on_update=algokit_utils.OnUpdate.AppendApp, + ) + + teal_program = f""" + #pragma version 10 + txn TypeEnum + int appl + == + txn ApplicationID + int {app_client.app_id} + == + && + txn RekeyTo + global ZeroAddress + == + && + txn Fee + global MinTxnFee + == + && + return + """ + + def compile_program(teal_program): + compiled_program = algod_client.compile(teal_program) + return base64.b64decode(compiled_program["result"]) + + program = compile_program(teal_program) + lsig = transaction.LogicSigAccount(program) + + # ******************** Plug_IN_Signer Account Generation *************************# + if False: + plug_in_sk, plug_in_addr = account.generate_account() + else: + plug_in_sk = """ + Cq9JfCTzMp9bSKVNxuF2YsNm0fS9RsshOVbN6I8Av5zbhEH2I1Qd8UN6UhgOfD1REDY9/pNjPy+D++ib2xTAAg== + """ + plug_in_addr = "3OCED5RDKQO7CQ32KIMA47B5KEIDMPP6SNRT6L4D7PUJXWYUYABBZG56JE" + # ******************** Plug_IN_Signer Public Signature *********************# + _, secret_key = nacl.bindings.crypto_sign_seed_keypair( + base64.b64decode(plug_in_sk)[: constants.key_len_bytes] + ) + message = constants.logic_prefix + program + raw_signed = nacl.bindings.crypto_sign(message, secret_key) + crypto_sign_BYTES = nacl.bindings.crypto_sign_BYTES + signature = nacl.encoding.RawEncoder.encode(raw_signed[:crypto_sign_BYTES]) + plug_in_public_sig = base64.b64encode(signature).decode() + + owner_vault_msig = transaction.Multisig(1, 1, [owner_addr, plug_in_addr]) + logger.info(f"Address owner_addr : {owner_addr}") + logger.info(f"Address owner_vault_msig : {owner_vault_msig.address()}") + logger.info(f"Address plug_in_addr : {plug_in_addr}") + logger.info(f"App add app_address : {app_client.app_address}") + logger.info(f"App ID app_id : {app_client.app_id}") + logger.info(f"Signer plug_in_public_sig: {plug_in_public_sig}") + + if int(algod_client.account_info(owner_vault_msig.address())["amount"]) == 0: + + # ********************* MSIG VAULT Generation *******************************# + # ********************* MSIG App Funding *******************************# + ptxn_app = transaction.PaymentTxn( + owner_addr, + sp, + app_client.app_address, + int(1e6) + ) + # ********************* MSIG VAUL Funding *******************************# + ptxn_vault = transaction.PaymentTxn( + owner_addr, + sp, + owner_vault_msig.address(), + int(1e6) + ) + + transaction.assign_group_id([ptxn_app, ptxn_vault]) + + signed_ptxn_app = ptxn_app.sign(owner_sk) + signed_ptxn_vault = ptxn_vault.sign(owner_sk) + + signed_group = [ + signed_ptxn_app, signed_ptxn_vault + ] + logger.info(signed_group) + txid = algod_client.send_transactions(signed_group) + result: Dict[str, Any] = transaction.wait_for_confirmation( + algod_client, txid, 4 + ) + logger.info(f"txID: {txid} confirmed in round: { + result.get('confirmed-round', 0)}") + + asa_creator_info = algod_client.account_info(asa_creator_addr) + if "assets" in asa_creator_info and (len(asa_creator_info["assets"]) > 0): + a_id = algod_client.account_info(asa_creator_addr)["assets"][0]["asset-id"] + else: + # ******************** asa_creator CREATE ASA ****************************# + logger.info("asa_creator Create ASA") + actxn = transaction.AssetConfigTxn( + sender=asa_creator_addr, + sp=sp, + default_frozen=False, + unit_name="rug2", + asset_name="2 Really Useful Gift", + manager=asa_creator_addr, + reserve=asa_creator_addr, + freeze=asa_creator_addr, + clawback=asa_creator_addr, + url="https://path/to/my/asset/details", + total=10, + decimals=0, + ) + sactxn = actxn.sign(asa_creator_sk) + tx_id = algod_client.send_transaction(sactxn) + logger.info(f"Sent asset create transaction with txid: {tx_id}") + # Wait for the transaction to be confirmed + results = transaction.wait_for_confirmation(algod_client, tx_id, 4) + a_id = results["asset-index"] + logger.info( + f"Result confirmed in round: { + results['confirmed-round']} ASA ID : {results['asset-index']}" + ) + logger.info(f"asa_creator created 10 ASA: {a_id}") + + if (int(algod_client.account_info(app_client.app_address)["amount"]) <= 151300): + ptxn_signer = transaction.PaymentTxn( + dispenser.address, sp, app_client.app_address, int(151300) + ).sign(dispenser.private_key) + algod_client.send_transaction(ptxn_signer) + logger.info(" Set public Sig") + response = app_client.set_public_sig( + account=owner_addr, + sig=plug_in_public_sig, + transaction_parameters=algokit_utils.TransactionParameters( + boxes=[(app_client.app_id, encoding.decode_address(owner_addr))], + accounts=[owner_addr] + ) + ) + logger.info(f"result: {response.return_value}") + + logger.info(" Get public Sig") + response = app_client.get_public_sig( + account=owner_addr, + transaction_parameters=algokit_utils.TransactionParameters( + boxes=[(app_client.app_id, encoding.decode_address(owner_addr))] + ), + ) + logger.info(f"result: {response.return_value}") + atc = AtomicTransactionComposer() + client = algokit_utils.ApplicationClient( + algod_client, + app_id=app_client.app_id, + app_spec=app_client.app_spec) + + composer = Composer(client, atc) + composer.opt_in( + id=a_id, + account=app_client.app_address, + transaction_parameters=algokit_utils.TransactionParameters( + foreign_assets=[a_id], signer=app_client.signer, + sender=owner_vault_msig.address() + ), + ) + opt_in_txn = composer.atc.txn_list[0].txn + + lsig.lsig.msig = owner_vault_msig + lsig.lsig.msig.subsigs[1].signature = base64.b64decode(plug_in_public_sig) + lstx = transaction.LogicSigTransaction(opt_in_txn, lsig) + optin_txid = algod_client.send_transaction(lstx) + + logger.info(f"Sent Msig Vault Opt-in with txid: {optin_txid}") + # Wait for the transaction to be confirmed + results = transaction.wait_for_confirmation(algod_client, optin_txid, 4) + logger.info(f"Result confirmed in round: {results['confirmed-round']}")