fix: 按所选签署主体指定易签宝发起企业 (#994)
This commit is contained in:
parent
dffc01dc51
commit
2efc0b766c
@ -574,6 +574,7 @@ class CourierAgreementSignatures extends Controller
|
||||
$signature = $this->quoteSignatureService->prepareVersion($version, user_id(), $data['signature_date']);
|
||||
$account = trim((string) $data['esign_account']);
|
||||
$signature = $this->quoteSignatureService->send($signature, [
|
||||
'signing_subject' => $data['signing_subject'],
|
||||
'organization_name' => $version->carrier_name,
|
||||
'operator_name' => trim((string) ($data['operator_name'] ?? '')) ?: $version->carrier_name,
|
||||
'operator_account' => $account,
|
||||
|
||||
@ -12,6 +12,8 @@ final class EsignClient
|
||||
|
||||
private readonly EsignRequestSigner $signer;
|
||||
|
||||
private readonly EsignConfiguration $configuration;
|
||||
|
||||
/**
|
||||
* 骑缝章中心的纵向坐标(PDF 坐标,原点在左下角,A4 高 841.89)。
|
||||
* 按企业签署方顺序依次取用:第一家偏上、第二家居中偏下、第三家靠下。
|
||||
@ -29,6 +31,7 @@ final class EsignClient
|
||||
$configuration ??= EsignConfiguration::fromConfig();
|
||||
$configuration->assertReadyForApi();
|
||||
|
||||
$this->configuration = $configuration;
|
||||
$this->baseUrl = $configuration->baseUrl();
|
||||
$this->signer = new EsignRequestSigner($configuration->appId(), $configuration->appSecret());
|
||||
}
|
||||
@ -94,8 +97,14 @@ final class EsignClient
|
||||
return $fileId;
|
||||
}
|
||||
|
||||
public function assertSignFlowInitiator(?string $organizationName = null): void
|
||||
{
|
||||
$this->configuration->signFlowInitiator($organizationName);
|
||||
}
|
||||
|
||||
public function createSignFlow(string $fileId, string $fileName, array $signer, int $expiresAtMs): string
|
||||
{
|
||||
$initiator = $this->configuration->signFlowInitiator($signer['initiator_name'] ?? null);
|
||||
$flowConfig = [
|
||||
'signFlowTitle' => $signer['flow_title'],
|
||||
'signFlowExpireTime' => $expiresAtMs,
|
||||
@ -160,11 +169,15 @@ final class EsignClient
|
||||
],
|
||||
];
|
||||
}
|
||||
$data = $this->request('POST', '/v3/sign-flow/create-by-file', [
|
||||
$payload = [
|
||||
'docs' => $docs,
|
||||
'signFlowConfig' => $flowConfig,
|
||||
'signers' => $signerPayload,
|
||||
]);
|
||||
];
|
||||
if ($initiator !== null) {
|
||||
$payload['signFlowInitiator'] = $initiator;
|
||||
}
|
||||
$data = $this->request('POST', '/v3/sign-flow/create-by-file', $payload);
|
||||
$signFlowId = (string) ($data['signFlowId'] ?? '');
|
||||
|
||||
if ($signFlowId === '') {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Esign;
|
||||
|
||||
use JsonException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
@ -27,6 +28,10 @@ final class EsignConfiguration
|
||||
|
||||
private readonly int $timeout;
|
||||
|
||||
private readonly string $initiatorAccounts;
|
||||
|
||||
private readonly string $defaultInitiator;
|
||||
|
||||
public function __construct(
|
||||
string $environment,
|
||||
string $baseUrl,
|
||||
@ -35,6 +40,8 @@ final class EsignConfiguration
|
||||
string $notifyUrl,
|
||||
int $callbackToleranceSeconds,
|
||||
int $timeout,
|
||||
string $initiatorAccounts = '{}',
|
||||
string $defaultInitiator = '',
|
||||
) {
|
||||
$this->environment = strtolower(trim($environment));
|
||||
$this->baseUrl = rtrim(trim($baseUrl), '/');
|
||||
@ -43,6 +50,8 @@ final class EsignConfiguration
|
||||
$this->notifyUrl = trim($notifyUrl);
|
||||
$this->callbackToleranceSeconds = max(60, $callbackToleranceSeconds);
|
||||
$this->timeout = max(5, $timeout);
|
||||
$this->initiatorAccounts = trim($initiatorAccounts);
|
||||
$this->defaultInitiator = trim($defaultInitiator);
|
||||
}
|
||||
|
||||
public static function fromConfig(): self
|
||||
@ -55,6 +64,8 @@ final class EsignConfiguration
|
||||
notifyUrl: trim((string) config('services.esign.notify_url')),
|
||||
callbackToleranceSeconds: max(60, (int) config('services.esign.callback_tolerance_seconds', 900)),
|
||||
timeout: max(5, (int) config('services.esign.timeout', 20)),
|
||||
initiatorAccounts: (string) config('services.esign.initiator_accounts', '{}'),
|
||||
defaultInitiator: (string) config('services.esign.default_initiator', ''),
|
||||
);
|
||||
}
|
||||
|
||||
@ -100,6 +111,35 @@ final class EsignConfiguration
|
||||
return $this->environment;
|
||||
}
|
||||
|
||||
/** Resolve the selected legal entity without changing credentials used by historical flows. */
|
||||
public function signFlowInitiator(?string $organizationName = null): ?array
|
||||
{
|
||||
try {
|
||||
$accounts = json_decode($this->initiatorAccounts ?: '{}', true, 32, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
throw new RuntimeException('E 签宝发起身份配置格式无效,请联系管理员。');
|
||||
}
|
||||
|
||||
if (! is_array($accounts)) {
|
||||
throw new RuntimeException('E 签宝发起身份配置格式无效,请联系管理员。');
|
||||
}
|
||||
if ($accounts === [] && $this->defaultInitiator === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = trim((string) $organizationName) ?: $this->defaultInitiator;
|
||||
$account = $accounts[$name] ?? null;
|
||||
if (! is_array($account) || ! is_string($account['org_id'] ?? null) || ! is_string($account['psn_id'] ?? null)
|
||||
|| trim($account['org_id']) === '' || trim($account['psn_id']) === '') {
|
||||
throw new RuntimeException('所选签署主体未配置完整的 E 签宝发起企业和经办人身份,请联系管理员。');
|
||||
}
|
||||
|
||||
return ['orgInitiator' => [
|
||||
'orgId' => trim($account['org_id']),
|
||||
'transactor' => ['psnId' => trim($account['psn_id'])],
|
||||
]];
|
||||
}
|
||||
|
||||
public function baseUrl(): string
|
||||
{
|
||||
return $this->baseUrl;
|
||||
|
||||
@ -116,6 +116,8 @@ final class CourierAgreementSignatureService
|
||||
try {
|
||||
$signers = $this->signersFor($signature);
|
||||
$client = app(EsignClient::class);
|
||||
$initiatorName = data_get($signature->payload, 'signing_subject');
|
||||
$client->assertSignFlowInitiator($initiatorName);
|
||||
$bytes = Storage::disk('uploads')->get($signature->source_pdf_path);
|
||||
$fileName = 'agreement-' . $signature->business_no . '.pdf';
|
||||
$fileId = $client->uploadPdf($bytes, $fileName);
|
||||
@ -124,6 +126,7 @@ final class CourierAgreementSignatureService
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
$signer = [
|
||||
'initiator_name' => $initiatorName,
|
||||
'flow_title' => $this->safeTitle($signature->company_full_name . ' ' . $signature->documentLabel()),
|
||||
'organization_name' => $signature->organization_name,
|
||||
'operator_name' => $signature->document_type === 'shared-contract'
|
||||
|
||||
@ -132,10 +132,12 @@ final class CourierQuoteSignatureService
|
||||
|
||||
try {
|
||||
$client = app(EsignClient::class);
|
||||
$client->assertSignFlowInitiator($data['signing_subject'] ?? null);
|
||||
$bytes = Storage::disk('uploads')->get($signature->source_pdf_path);
|
||||
$fileName = 'quote-' . $signature->version->version_code . '.pdf';
|
||||
$fileId = $client->uploadPdf($bytes, $fileName);
|
||||
$signer = [
|
||||
'initiator_name' => $data['signing_subject'] ?? null,
|
||||
'flow_title' => $signature->version->carrier_name . ' ' . $signature->version->version_code . ' 报价确认',
|
||||
'organization_name' => $signature->organization_name,
|
||||
'operator_name' => $signature->operator_name,
|
||||
|
||||
@ -47,6 +47,8 @@ return [
|
||||
),
|
||||
'app_id' => env('ESIGN_APP_ID'),
|
||||
'app_secret' => env('ESIGN_APP_SECRET'),
|
||||
'initiator_accounts' => env('ESIGN_INITIATOR_ACCOUNTS', '{}'),
|
||||
'default_initiator' => env('ESIGN_DEFAULT_INITIATOR', ''),
|
||||
'notify_url' => env('ESIGN_NOTIFY_URL'),
|
||||
'callback_tolerance_seconds' => (int) env('ESIGN_CALLBACK_TOLERANCE_SECONDS', 900),
|
||||
'timeout' => (int) env('ESIGN_TIMEOUT', 20),
|
||||
|
||||
@ -33,6 +33,21 @@ ESIGN_TIMEOUT=20
|
||||
|
||||
在 E签宝开放平台完成:企业认证、SaaS API V3 应用、出口 IP 白名单、Webhook、`SIGN_FLOW_COMPLETE` 订阅,以及短信/邮件和企业签署服务开通。
|
||||
|
||||
### 指定合同发起企业
|
||||
|
||||
当签署主体与应用所属企业不同时,在私有 `runtime/.env` 配置精确企业名称与实名账号 ID 的对应关系:
|
||||
|
||||
```dotenv
|
||||
ESIGN_DEFAULT_INITIATOR="示例发起企业"
|
||||
ESIGN_INITIATOR_ACCOUNTS='{"示例发起企业":{"org_id":"企业实名账号ID","psn_id":"我方经办人实名账号ID"}}'
|
||||
```
|
||||
|
||||
协议和报价确认单按页面选择的 `signing_subject` 查找对应身份;旧报价推送入口未选择主体时使用配置中的默认企业。每个企业必须单独核对认证、有效的 `org_initiate_sign` 授权及经办人的 `psn_initiate_sign` 授权,不能使用对方快递经办人的账号。真实企业和经办人 ID 只保存在私有配置中。
|
||||
|
||||
启用对应关系后,缺少所选企业或经办人配置时会在上传前拒绝推送,不再静默使用应用所属企业。未启用时保持应用自身发起的接入方式。配置只用于新建流程,不切换 App ID/AppSecret,历史流程的查询、回调和归档继续使用原应用。
|
||||
|
||||
e签宝要求高级版通过“关联企业”开通,生态伙伴版通过企业和经办人授权;套餐能力以e签宝实际开通结果为准。参考[指定合同发起方](https://open.esign.cn/doc/opendoc/pdf-sign3/su5g42)。
|
||||
|
||||
## 联调步骤
|
||||
|
||||
1. 在页面“协议签署 → 发起签章”选择文件类型,填写该类型专属字段和对方实名绑定的手机号/邮箱。
|
||||
|
||||
@ -22,7 +22,11 @@ class CourierAgreementCredentialsTest extends FeatureTestCase
|
||||
Storage::fake('uploads');
|
||||
config(['app.key' => 'base64:' . base64_encode(str_repeat('k', 32)),
|
||||
'services.esign.environment' => 'sandbox', 'services.esign.base_url' => 'https://smlopenapi.esign.test',
|
||||
'services.esign.app_id' => 'sandbox-test-id', 'services.esign.app_secret' => 'sandbox-test-secret']);
|
||||
'services.esign.app_id' => 'sandbox-test-id', 'services.esign.app_secret' => 'sandbox-test-secret',
|
||||
'services.esign.initiator_accounts' => json_encode([
|
||||
$this->company->name => ['org_id' => 'org-selected', 'psn_id' => 'person-selected'],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
'services.esign.default_initiator' => '']);
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
}
|
||||
|
||||
@ -69,10 +73,18 @@ class CourierAgreementCredentialsTest extends FeatureTestCase
|
||||
Http::assertSent(function (ClientRequest $request): bool {
|
||||
if (! str_ends_with($request->url(), '/create-by-file')) return false;
|
||||
$data = $request->data();
|
||||
$this->assertSame('org-selected', data_get($data, 'signFlowInitiator.orgInitiator.orgId'));
|
||||
$this->assertSame('person-selected', data_get($data, 'signFlowInitiator.orgInitiator.transactor.psnId'));
|
||||
$this->assertCount(3, $data['docs']);
|
||||
$this->assertCount(3, $data['signers'][0]['signFields']);
|
||||
$this->assertCount(1, $data['signers'][1]['signFields']);
|
||||
foreach (array_slice($data['signers'][0]['signFields'], 1) as $field) {
|
||||
// Multi-page main documents also carry an across-page seal; credential fields remain separate.
|
||||
$normalFields = array_map(static fn (array $signer): array => array_values(array_filter(
|
||||
$signer['signFields'],
|
||||
static fn (array $field): bool => data_get($field, 'normalSignFieldConfig.signFieldStyle') !== 2,
|
||||
)), $data['signers']);
|
||||
$this->assertCount(3, $normalFields[0]);
|
||||
$this->assertCount(1, $normalFields[1]);
|
||||
foreach (array_slice($normalFields[0], 1) as $index => $field) {
|
||||
$this->assertSame($data['docs'][$index + 1]['fileId'], $field['fileId']);
|
||||
$this->assertSame('1', $field['normalSignFieldConfig']['signFieldPosition']['positionPage']);
|
||||
$this->assertFalse($field['normalSignFieldConfig']['freeMode']);
|
||||
}
|
||||
|
||||
@ -35,6 +35,11 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
||||
'services.esign.app_id' => 'sandbox-app-id',
|
||||
'services.esign.app_secret' => 'sandbox-app-secret',
|
||||
'services.esign.notify_url' => null,
|
||||
'services.esign.initiator_accounts' => json_encode([
|
||||
$this->company->name => ['org_id' => 'org-selected', 'psn_id' => 'person-selected'],
|
||||
'默认企业' => ['org_id' => 'org-default', 'psn_id' => 'person-default'],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
'services.esign.default_initiator' => '默认企业',
|
||||
]);
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
}
|
||||
@ -68,7 +73,9 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
||||
return false;
|
||||
}
|
||||
|
||||
return data_get($request->data(), 'signers.0.signerType') === 1
|
||||
return data_get($request->data(), 'signFlowInitiator.orgInitiator.orgId') === 'org-selected'
|
||||
&& data_get($request->data(), 'signFlowInitiator.orgInitiator.transactor.psnId') === 'person-selected'
|
||||
&& data_get($request->data(), 'signers.0.signerType') === 1
|
||||
&& data_get($request->data(), 'signers.0.orgSignerInfo.orgName') === '新北中通快递有限公司'
|
||||
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '张经理';
|
||||
});
|
||||
@ -78,6 +85,23 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
||||
$this->assertSame(1, CourierAgreementSignature::query()->count());
|
||||
}
|
||||
|
||||
public function test_missing_selected_initiator_fails_before_upload_without_unknown_status(): void
|
||||
{
|
||||
config(['services.esign.initiator_accounts' => '{}']);
|
||||
Http::fake();
|
||||
|
||||
$this->withExceptionHandling()->loginAs()
|
||||
->post(route('business.courier-agreement-signatures.store'), $this->accountProofPayload('missing-initiator'))
|
||||
->assertSessionHasErrors('signature');
|
||||
|
||||
Http::assertNothingSent();
|
||||
$signature = CourierAgreementSignature::query()->firstOrFail();
|
||||
$this->assertSame(CourierAgreementSignature::STATUS_FAILED, $signature->status);
|
||||
$this->assertNull($signature->esign_file_id);
|
||||
$this->assertNull($signature->esign_sign_flow_id);
|
||||
$this->assertStringContainsString('所选签署主体未配置完整', $signature->last_error);
|
||||
}
|
||||
|
||||
public function test_authorized_business_user_can_push_without_becoming_an_administrator(): void
|
||||
{
|
||||
$this->user->syncRoles([Role::where('name', 'user')->firstOrFail()->id]);
|
||||
@ -1054,7 +1078,9 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
||||
|
||||
$signers = $request->data()['signers'] ?? [];
|
||||
|
||||
return count($signers) === 1
|
||||
return data_get($request->data(), 'signFlowInitiator.orgInitiator.orgId') === 'org-selected'
|
||||
&& data_get($request->data(), 'signFlowInitiator.orgInitiator.transactor.psnId') === 'person-selected'
|
||||
&& count($signers) === 1
|
||||
&& ($signers[0]['signerType'] ?? null) === 1
|
||||
&& ($signers[0]['orgSignerInfo']['orgName'] ?? null) === '新北中通'
|
||||
&& ($signers[0]['orgSignerInfo']['transactorInfo']['psnAccount'] ?? null) === '13800138000';
|
||||
|
||||
@ -27,6 +27,8 @@ class CourierQuoteSignatureWorkflowTest extends FeatureTestCase
|
||||
'services.esign.app_id' => 'sandbox-app-id',
|
||||
'services.esign.app_secret' => 'sandbox-app-secret',
|
||||
'services.esign.notify_url' => null,
|
||||
'services.esign.initiator_accounts' => '{"默认企业":{"org_id":"org-default","psn_id":"person-default"}}',
|
||||
'services.esign.default_initiator' => '默认企业',
|
||||
]);
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
}
|
||||
@ -124,12 +126,38 @@ class CourierQuoteSignatureWorkflowTest extends FeatureTestCase
|
||||
|
||||
$payload = json_decode($request->body(), true);
|
||||
|
||||
return data_get($payload, 'signers.0.orgSignerInfo.orgName') === '新北中通快递有限公司'
|
||||
return data_get($payload, 'signFlowInitiator.orgInitiator.orgId') === 'org-default'
|
||||
&& data_get($payload, 'signFlowInitiator.orgInitiator.transactor.psnId') === 'person-default'
|
||||
&& data_get($payload, 'signers.0.orgSignerInfo.orgName') === '新北中通快递有限公司'
|
||||
&& data_get($payload, 'signers.0.normalSignFieldConfig') === null
|
||||
&& data_get($payload, 'signers.0.signFields.0.normalSignFieldConfig.freeMode') === true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_unmapped_quote_initiator_stops_before_upload(): void
|
||||
{
|
||||
$signature = $this->createPreparedSignature();
|
||||
Http::fake();
|
||||
|
||||
try {
|
||||
app(CourierQuoteSignatureService::class)->send($signature, [
|
||||
'signing_subject' => '未配置企业',
|
||||
'organization_name' => '测试快递有限公司',
|
||||
'operator_name' => '张经理',
|
||||
'operator_account' => '13800138000',
|
||||
'notice_type' => '1',
|
||||
'expires_at' => now()->addDays(7),
|
||||
], $this->user->id);
|
||||
$this->fail('An unmapped company must not fall back to the default initiator.');
|
||||
} catch (\Illuminate\Validation\ValidationException $exception) {
|
||||
$this->assertStringContainsString('所选签署主体未配置完整', $exception->errors()['signature'][0]);
|
||||
}
|
||||
|
||||
Http::assertNothingSent();
|
||||
$this->assertSame(CourierQuoteSignature::STATUS_FAILED, $signature->fresh()->status);
|
||||
$this->assertNull($signature->fresh()->esign_sign_flow_id);
|
||||
}
|
||||
|
||||
public function test_tampered_source_pdf_is_not_sent(): void
|
||||
{
|
||||
$signature = $this->createPreparedSignature();
|
||||
@ -232,6 +260,7 @@ class CourierQuoteSignatureWorkflowTest extends FeatureTestCase
|
||||
|
||||
public function test_manual_sync_downloads_signed_pdf_and_completion_certificate(): void
|
||||
{
|
||||
config(['services.esign.initiator_accounts' => '{invalid']);
|
||||
$signature = $this->createPreparedSignature([
|
||||
'status' => CourierQuoteSignature::STATUS_SIGNING,
|
||||
'esign_file_id' => 'file-001',
|
||||
|
||||
@ -56,11 +56,65 @@ class EsignConfigurationTest extends TestCase
|
||||
$configuration->assertReadyForApi();
|
||||
}
|
||||
|
||||
public function test_selected_company_overrides_the_default_initiator(): void
|
||||
{
|
||||
$configuration = $this->configuration(
|
||||
initiatorAccounts: json_encode([
|
||||
'默认企业' => ['org_id' => 'org-default', 'psn_id' => 'person-default'],
|
||||
'所选企业' => ['org_id' => 'org-selected', 'psn_id' => 'person-selected'],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
defaultInitiator: '默认企业',
|
||||
);
|
||||
|
||||
$this->assertSame(['orgInitiator' => [
|
||||
'orgId' => 'org-selected', 'transactor' => ['psnId' => 'person-selected'],
|
||||
]], $configuration->signFlowInitiator('所选企业'));
|
||||
$this->assertSame('org-default', $configuration->signFlowInitiator()['orgInitiator']['orgId']);
|
||||
}
|
||||
|
||||
public function test_unconfigured_installations_keep_the_application_initiator(): void
|
||||
{
|
||||
$this->assertNull($this->configuration()->signFlowInitiator('旧企业'));
|
||||
}
|
||||
|
||||
public function test_unmapped_company_cannot_silently_use_the_default_initiator(): void
|
||||
{
|
||||
$configuration = $this->configuration(
|
||||
initiatorAccounts: '{"默认企业":{"org_id":"org-default","psn_id":"person-default"}}',
|
||||
defaultInitiator: '默认企业',
|
||||
);
|
||||
$this->expectExceptionMessage('所选签署主体未配置完整');
|
||||
$configuration->signFlowInitiator('另一企业');
|
||||
}
|
||||
|
||||
/** @dataProvider invalidInitiatorAccounts */
|
||||
public function test_invalid_initiator_configuration_prevents_new_flows(string $accounts): void
|
||||
{
|
||||
$configuration = $this->configuration(initiatorAccounts: $accounts, defaultInitiator: '企业');
|
||||
// Historical reads still use the existing application credentials.
|
||||
$configuration->assertReadyForApi();
|
||||
$this->expectException(RuntimeException::class);
|
||||
$configuration->signFlowInitiator('企业');
|
||||
}
|
||||
|
||||
public static function invalidInitiatorAccounts(): array
|
||||
{
|
||||
return [
|
||||
'invalid json' => ['{'],
|
||||
'invalid type' => ['true'],
|
||||
'missing transactor' => ['{"企业":{"org_id":"org-a"}}'],
|
||||
'blank organization' => ['{"企业":{"org_id":" ","psn_id":"person-a"}}'],
|
||||
'invalid account type' => ['{"企业":{"org_id":"org-a","psn_id":123}}'],
|
||||
];
|
||||
}
|
||||
|
||||
private function configuration(
|
||||
string $environment = 'production',
|
||||
string $baseUrl = 'https://openapi.esign.cn',
|
||||
string $appId = 'production-app-id',
|
||||
string $appSecret = 'production-app-secret',
|
||||
string $initiatorAccounts = '{}',
|
||||
string $defaultInitiator = '',
|
||||
): EsignConfiguration {
|
||||
return new EsignConfiguration(
|
||||
environment: $environment,
|
||||
@ -70,6 +124,8 @@ class EsignConfigurationTest extends TestCase
|
||||
notifyUrl: '',
|
||||
callbackToleranceSeconds: 900,
|
||||
timeout: 20,
|
||||
initiatorAccounts: $initiatorAccounts,
|
||||
defaultInitiator: $defaultInitiator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user